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:
@@ -626,13 +626,7 @@ class TicketController extends Controller
|
||||
->update(['ticket_id' => $ticket->id]);
|
||||
|
||||
// Add a system note marking the merge
|
||||
TicketMessage::create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => Auth::id(),
|
||||
'body' => "Merged from ticket {$source->number}.",
|
||||
'is_internal' => true,
|
||||
'source' => 'system',
|
||||
]);
|
||||
TicketMessage::system($ticket, "Merged from ticket {$source->number}.");
|
||||
|
||||
// Add source's submitter as a participant (if not already the submitter of target)
|
||||
if ($source->submitter_id !== $ticket->submitter_id) {
|
||||
@@ -672,16 +666,31 @@ class TicketController extends Controller
|
||||
'submitter_id' => $ticket->submitter_id,
|
||||
'assigned_to' => $ticket->assigned_to,
|
||||
'project_id' => $ticket->project_id,
|
||||
'title' => $ticket->title . ' (split)',
|
||||
'title' => static::splitTitle($ticket->title),
|
||||
'description' => $ticket->description,
|
||||
'status' => 'open',
|
||||
'priority_id' => $ticket->priority_id,
|
||||
'due_date' => $ticket->due_date,
|
||||
]);
|
||||
|
||||
// Duplicate messages (exclude system notes)
|
||||
/*
|
||||
* Duplicate messages, EXCLUDING system notes.
|
||||
*
|
||||
* This comment has always said "exclude" and the code never did. It cost
|
||||
* nothing while the enum rejected 'system', because no system row could
|
||||
* exist to copy — #415 makes them writable, so without this a split of a
|
||||
* previously-merged ticket would carry "Merged from ticket QAX-0005." into
|
||||
* a brand-new ticket that was never merged with anything.
|
||||
*
|
||||
* A system note is an audit record of something that happened to THIS
|
||||
* ticket. The copy gets its own, written below.
|
||||
*/
|
||||
$ticket->load('messages');
|
||||
foreach ($ticket->messages as $msg) {
|
||||
if ($msg->isSystem()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TicketMessage::create([
|
||||
'ticket_id' => $newTicket->id,
|
||||
'user_id' => $msg->user_id,
|
||||
@@ -693,20 +702,8 @@ class TicketController extends Controller
|
||||
}
|
||||
|
||||
// Cross-reference notes on both tickets
|
||||
TicketMessage::create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => Auth::id(),
|
||||
'body' => "Split: new ticket {$newTicket->number} created from this ticket.",
|
||||
'is_internal' => true,
|
||||
'source' => 'system',
|
||||
]);
|
||||
TicketMessage::create([
|
||||
'ticket_id' => $newTicket->id,
|
||||
'user_id' => Auth::id(),
|
||||
'body' => "Split from ticket {$ticket->number}.",
|
||||
'is_internal' => true,
|
||||
'source' => 'system',
|
||||
]);
|
||||
TicketMessage::system($ticket, "Split: new ticket {$newTicket->number} created from this ticket.");
|
||||
TicketMessage::system($newTicket, "Split from ticket {$ticket->number}.");
|
||||
|
||||
return $newTicket;
|
||||
});
|
||||
@@ -715,6 +712,39 @@ class TicketController extends Controller
|
||||
->with('success', "Ticket {$newTicket->number} created from split.");
|
||||
}
|
||||
|
||||
/**
|
||||
* The split ticket's title: the original plus a suffix, guaranteed to fit.
|
||||
*
|
||||
* `tickets.title` is varchar(255) and both store() and update() validate the
|
||||
* title at `max:255`. Appending " (split)" to a title already at the limit
|
||||
* produced 263 characters and the same hard 500 as #415 — a second, unreported
|
||||
* instance of it on the very action being fixed, needing only a long enough
|
||||
* title to trigger.
|
||||
*
|
||||
* ── WHY THIS IS BOUNDED AND NOT WIDENED ─────────────────────────────────
|
||||
*
|
||||
* The sibling defect in central-radius (#410) was fixed the other way: a
|
||||
* 510-character skip reason was widened into the column rather than shortened,
|
||||
* because the reason was the point and the column was simply too small.
|
||||
*
|
||||
* The rule that separates them is what the limit MEANS. 255 here is not an
|
||||
* incidental column width — it is the product's own declared, user-facing title
|
||||
* limit, enforced by two validation rules. Widening the column to fit 263 would
|
||||
* mint rows the edit form then refuses to save, so the ticket could never be
|
||||
* corrected by anyone: a row the product will not let you fix is worse than a
|
||||
* truncated title. So: widen when the data is right and the column is wrong;
|
||||
* bound when the column expresses a real product rule.
|
||||
*
|
||||
* Multibyte-safe by character, because varchar(255) counts characters.
|
||||
*/
|
||||
private static function splitTitle(string $title): string
|
||||
{
|
||||
$suffix = ' (split)';
|
||||
$room = 255 - mb_strlen($suffix);
|
||||
|
||||
return (mb_strlen($title) > $room ? mb_substr($title, 0, $room) : $title).$suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a participant to a ticket.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user