#4781: the nightly auto-close is on the record too
`ticketing:auto-close` closed tickets with a single bulk UPDATE, which fires no model events. With the audit declaration in place that left one way of closing a ticket invisible: the Changes log would show a ticket going open → resolved and then nothing, while the row itself read `closed`. "Who closed this" is the question the trail exists to answer and "the nightly sweep did" is a real answer — but only if the sweep writes it. Closed one at a time through the model, chunked by id so a first sweep over a backlog does not load every resolved ticket at once. Paging by id is what makes that safe while the filtered column is being written.
This commit is contained in:
@@ -22,9 +22,39 @@ class AutoCloseTickets extends Command
|
||||
|
||||
$cutoff = now()->subDays($days);
|
||||
|
||||
$count = Ticket::where('status', 'resolved')
|
||||
/*
|
||||
* CLOSED ONE AT A TIME, THROUGH THE MODEL — [central-ticketing] #4781.
|
||||
*
|
||||
* This was a single `->update(['status' => 'closed'])`, which is one
|
||||
* statement and no model events. Since #4781 a ticket's status change is
|
||||
* an audited fact, and a bulk update would have left exactly one way of
|
||||
* closing a ticket invisible: the Changes log would show a ticket going
|
||||
* open → resolved and then nothing, while the row itself said `closed`.
|
||||
* "Who closed this" is the question the audit trail exists to answer,
|
||||
* and "the nightly sweep did, on this date" is a real answer — but only
|
||||
* if the sweep writes it.
|
||||
*
|
||||
* Per-ticket rather than one summary event because the answer has to be
|
||||
* on THE TICKET: the per-record history panel reads rows whose subject
|
||||
* is that ticket, and an aggregate "closed 14 tickets" row would not
|
||||
* appear in any of their histories.
|
||||
*
|
||||
* `chunkById`, so a first sweep over a long backlog does not load every
|
||||
* resolved ticket into memory at once. Paging by id is what makes this
|
||||
* safe while the filtered column is being written: each page continues
|
||||
* from the last id seen rather than from an offset, so rows dropping out
|
||||
* of `status = 'resolved'` cannot cause a page to be skipped.
|
||||
*/
|
||||
$count = 0;
|
||||
|
||||
Ticket::where('status', 'resolved')
|
||||
->where('updated_at', '<=', $cutoff)
|
||||
->update(['status' => 'closed']);
|
||||
->chunkById(200, function ($tickets) use (&$count) {
|
||||
foreach ($tickets as $ticket) {
|
||||
$ticket->forceFill(['status' => 'closed'])->save();
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
$this->info("Auto-closed {$count} ticket(s) resolved more than {$days} day(s) ago.");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user