From c892d9dcc809b23d3af101d4682a36b2283512f9 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 8 Sep 2026 11:45:11 -0400 Subject: [PATCH] Recompute next_run_at when re-enabling a paused recurring schedule (#89) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toggleActive() flipped is_active without touching next_run_at. If a schedule was disabled while next_run_at was still in the future, then re-enabled after that date had passed, the next cron tick saw next_run_at <= NOW() and fired immediately — surprising for an admin expecting a re-enabled "daily" schedule to wait until its next natural occurrence. Now recomputes next_run_at from the current time when transitioning to active, matching what a fresh schedule creation would produce; disabling is unchanged. Verified against a local MariaDB instance: re-enabling a schedule whose next_run_at was in 2020 recomputed it to tomorrow at the scheduled time; disabling leaves next_run_at untouched. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MGDKHiU5RJdo3dqQUDow3X --- models/RecurringTicketModel.php | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/models/RecurringTicketModel.php b/models/RecurringTicketModel.php index 58a1cf0..d6dc988 100644 --- a/models/RecurringTicketModel.php +++ b/models/RecurringTicketModel.php @@ -231,9 +231,33 @@ class RecurringTicketModel */ public function toggleActive($recurringId) { - $sql = "UPDATE recurring_tickets SET is_active = NOT is_active WHERE recurring_id = ?"; - $stmt = $this->conn->prepare($sql); - $stmt->bind_param('i', $recurringId); + $recurring = $this->getById($recurringId); + if (!$recurring) { + return ['success' => false]; + } + + $newActive = $recurring['is_active'] ? 0 : 1; + + if ($newActive) { + // Re-enabling: recompute next_run_at from now, as if the schedule + // were freshly created. Otherwise a schedule paused while + // next_run_at was still in the future, then re-enabled after that + // date has passed, would fire immediately on the next cron tick + // instead of waiting for its next natural occurrence. + $nextRun = $this->calculateNextRunTime( + $recurring['schedule_type'], + $recurring['schedule_day'], + $recurring['schedule_time'] + ); + $sql = "UPDATE recurring_tickets SET is_active = ?, next_run_at = ? WHERE recurring_id = ?"; + $stmt = $this->conn->prepare($sql); + $stmt->bind_param('isi', $newActive, $nextRun, $recurringId); + } else { + $sql = "UPDATE recurring_tickets SET is_active = ? WHERE recurring_id = ?"; + $stmt = $this->conn->prepare($sql); + $stmt->bind_param('ii', $newActive, $recurringId); + } + $success = $stmt->execute(); $stmt->close(); return ['success' => $success];