Persist status-change comments transactionally with the update (#37)

A comment accompanying a status change (required or user-supplied) was
posted via a separate, independent HTTP call/write (add_comment.php,
or a second add_comment call in lt.ticketStatus.submit()'s
requires_comment retry path) before the status update itself. A failure
partway through — or the client never issuing the second call — could
leave a "reason" comment persisted with no matching status change, or
vice versa, with no rollback tying the two together.

api/update_ticket.php and api/ticket_status_api.php now post the comment
and apply the status update inside one transaction, rolling back both on
any failure. assets/js/ticket.js and lt.ticketStatus.submit() in
assets/js/base.js no longer make a separate add_comment.php call; they
pass the comment directly to update_ticket.php, which persists it
server-side alongside the status change.

Verified against real MariaDB by extracting the live ApiTicketController
and the ticket_status_api.php transaction logic and running them
directly: a forced optimistic-lock conflict correctly rolled back both
the comment and the status change, and a successful call persisted
exactly one comment alongside the status change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117oBw2jN4kALYeS8HPq4zV
This commit is contained in:
2026-09-11 21:41:48 -04:00
co-authored by Claude Sonnet 5
parent d205a9577a
commit 310dcd0840
4 changed files with 98 additions and 61 deletions
+8 -5
View File
@@ -2858,8 +2858,11 @@
TICKET STATUS CHANGE (comment-aware)
lt.ticketStatus.submit(ticketId, newStatus, { comment? }) → Promise<data>
Posts /api/update_ticket.php. If the server rejects with
requires_comment, opens a comment modal, persists the comment via
/api/add_comment.php, then retries the update once WITH the comment.
requires_comment, opens a comment modal, then retries the update once
WITH the comment — update_ticket.php persists it in the same DB
transaction as the status change itself, so there's no separate
add_comment.php call that could leave an orphaned comment if the
status update then failed.
Rejects with err.cancelled === true if the user cancels the modal.
================================================================ */
function _statusCommentModal(newStatus) {
@@ -2922,9 +2925,9 @@
cancelErr.cancelled = true;
throw cancelErr;
}
// Persist the comment, then retry the status change with it included.
return api.post('/api/add_comment.php', { ticket_id: id, comment_text: comment })
.then(() => api.post('/api/update_ticket.php', { ticket_id: id, status: newStatus, comment: comment }));
// Retry with the comment included — update_ticket.php persists it
// transactionally with the status update itself.
return api.post('/api/update_ticket.php', { ticket_id: id, status: newStatus, comment: comment });
});
});
},
+7 -6
View File
@@ -713,12 +713,13 @@ function updateTicketStatus() {
return;
}
cleanup(true);
// Post comment first (persists it), then change status with the same
// comment included so the server's requires_comment check passes.
const ticketId = getTicketIdFromUrl();
lt.api.post('/api/add_comment.php', { ticket_id: ticketId, comment_text: comment })
.then(() => performStatusChange(statusSelect, selectedOption, newStatus, comment))
.catch(() => performStatusChange(statusSelect, selectedOption, newStatus, comment));
// The comment is sent as part of the status-change request itself
// (update_ticket.php persists it in the same DB transaction as the
// status update) rather than as a separate prior add_comment.php
// call — previously those were two independent, non-transactional
// writes, so a failure partway through could leave the "reason"
// comment persisted with no matching status change ever applied.
performStatusChange(statusSelect, selectedOption, newStatus, comment);
});
// Focus textarea on open
setTimeout(() => { const ta = document.getElementById(`${modalId}_comment`); if (ta) ta.focus(); }, 100);