diff --git a/cron/create_recurring_tickets.php b/cron/create_recurring_tickets.php index f2b851d..49ab526 100644 --- a/cron/create_recurring_tickets.php +++ b/cron/create_recurring_tickets.php @@ -29,6 +29,38 @@ function logMessage($message) echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n"; } +/** + * Record a recurring-ticket occurrence that was claimed (next_run_at already + * advanced to the next future run) but then failed to actually produce a + * ticket. That claim-then-fail ordering is deliberate — it stops a failing + * creation from re-firing and flooding duplicates on every subsequent cron + * tick — but means this specific occurrence has no other record anywhere an + * admin would normally look: no audit_log entry (nothing was created), no + * Matrix "ticket created" alert, no failure table. Without this, it's simply + * gone, silently, forever. + */ +function recordMissedOccurrence($auditLog, $recurring, $reason) +{ + $auditLog->log( + $recurring['created_by'], + 'error', + 'recurring_ticket', + (string)$recurring['recurring_id'], + [ + 'reason' => $reason, + 'title_template' => $recurring['title_template'], + 'schedule_type' => $recurring['schedule_type'], + ] + ); + + NotificationHelper::sendSystemAlert( + "Recurring ticket occurrence lost: schedule #{$recurring['recurring_id']} " + . "(\"{$recurring['title_template']}\") was claimed for this run but ticket " + . "creation failed, so this occurrence will not be created or retried.", + ['reason' => $reason, 'recurring_id' => $recurring['recurring_id']] + ); +} + logMessage("Starting recurring tickets cron job"); try { @@ -100,11 +132,17 @@ try { $created++; } else { - logMessage("ERROR: Failed to create ticket - " . ($result['error'] ?? 'Unknown error')); + $reason = $result['error'] ?? 'Unknown error'; + logMessage("ERROR: Failed to create ticket - " . $reason); + recordMissedOccurrence($auditLog, $recurring, $reason); $errors++; } } catch (Exception $e) { logMessage("ERROR: Exception processing recurring ticket - " . $e->getMessage()); + // claimForRun() already advanced next_run_at before this point, so + // this occurrence is permanently gone unless recorded somewhere an + // admin would actually look — a cron log line alone doesn't count. + recordMissedOccurrence($auditLog, $recurring, $e->getMessage()); $errors++; } } diff --git a/helpers/NotificationHelper.php b/helpers/NotificationHelper.php index 4ffa01a..f4590e5 100644 --- a/helpers/NotificationHelper.php +++ b/helpers/NotificationHelper.php @@ -59,6 +59,23 @@ class NotificationHelper // ─── Public event methods ───────────────────────────────────────────────── + /** + * Generic operational alert with no associated ticket (e.g. a recurring + * schedule whose ticket creation failed after its next_run_at was + * already advanced, so the missed occurrence has no other record an + * admin would normally see). Always sent to the shared + * MATRIX_NOTIFY_USERS list, regardless of any per-event notify toggle. + */ + public static function sendSystemAlert(string $message, array $context = []): void + { + self::fire(array_merge([ + 'event' => 'system_alert', + 'message' => $message, + ], $context, [ + 'notify_users' => self::notifyUsers(), + ])); + } + /** * New ticket created (manual or automated/API). *