Files
tinker_tickets/cron/create_recurring_tickets.php
jaredandClaude Opus 4.8 d6214a0339
Lint / PHP (phpcs PSR-12) (push) Successful in 26s
Lint / JS (eslint) (push) Successful in 12s
Lint / PHP requirements (version + extensions) (push) Successful in 21s
Security / PHP Security (semgrep) (push) Successful in 1m11s
Lint / Deploy (push) Successful in 2s
Lint / Notify on failure (push) Has been skipped
Lint / PHP (phpcs PSR-12) (pull_request) Successful in 47s
Lint / JS (eslint) (pull_request) Successful in 12s
Lint / PHP requirements (version + extensions) (pull_request) Successful in 59s
Security / PHP Security (semgrep) (pull_request) Successful in 1m6s
Lint / Deploy (pull_request) Has been skipped
Lint / Notify on failure (pull_request) Has been skipped
Add schema baseline, fix cron/retention, restore cleanup, correct docs
- migrations/000_baseline.sql: full schema baseline captured from prod
  (validated on a throwaway DB: 17 tables/17 FKs), so the schema is
  reproducible for fresh installs / disaster recovery
- create_recurring_tickets cron: send the Matrix ticket-created
  notification and invalidate the stats cache like the other create paths
- create_ticket_api.php + TicketController::create: invalidate the stats
  cache on create/escalate/reopen so dashboard counts aren't stale
- scripts/cleanup_orphan_uploads.php: restored, made safe (24h mtime
  grace, 9-digit-dir only, skips avatars/symlinks, matches the unique
  filename column, --dry-run)
- cron/cleanup_audit_log.php: enforce the configured audit-log retention
  (deleteOldLogs was implemented but never called)
- README: correct CSRF-rotation, hwmon dedup (no 24h window), SLA (no P3),
  stats-cache callers, and the project structure/endpoint listing
- .env.example: document TRUSTED_PROXIES fail-open risk and .env quoting

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:11:26 -04:00

150 lines
5.1 KiB
PHP

#!/usr/bin/env php
<?php
/**
* Recurring Tickets Cron Job
*
* Run this script via cron to automatically create tickets from recurring schedules.
* Recommended: run every 5-15 minutes.
*
* Example crontab entry (minute 10 of every hour):
* 10 * * * * /usr/bin/php /path/to/cron/create_recurring_tickets.php >> /var/log/recurring_tickets.log 2>&1
*/
// Change to project root directory
chdir(dirname(__DIR__));
// Include required files
require_once 'config/config.php';
require_once 'helpers/Database.php';
require_once 'helpers/NotificationHelper.php';
require_once 'models/RecurringTicketModel.php';
require_once 'models/TicketModel.php';
require_once 'models/AuditLogModel.php';
require_once 'models/StatsModel.php';
// Log function
function logMessage($message)
{
echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n";
}
logMessage("Starting recurring tickets cron job");
try {
// Create database connection (Database::getConnection sets utf8mb4 so
// non-ASCII titles/descriptions aren't corrupted on insert).
$conn = Database::getConnection();
// Initialize models
$recurringModel = new RecurringTicketModel($conn);
$ticketModel = new TicketModel($conn);
$auditLog = new AuditLogModel($conn);
// Get all due recurring tickets
$dueTickets = $recurringModel->getDueRecurringTickets();
logMessage("Found " . count($dueTickets) . " recurring tickets due for creation");
$created = 0;
$errors = 0;
foreach ($dueTickets as $recurring) {
logMessage("Processing recurring ticket ID: " . $recurring['recurring_id']);
try {
// Claim the schedule FIRST (atomic advance of next_run_at). If another
// cron run already claimed it, or it's no longer due, skip it — this
// prevents duplicate-ticket floods if a later step throws.
if (!$recurringModel->claimForRun($recurring['recurring_id'])) {
logMessage("Skipped (already claimed or not due): " . $recurring['recurring_id']);
continue;
}
// Prepare ticket data
$ticketData = [
'title' => processTemplate($recurring['title_template']),
'description' => processTemplate($recurring['description_template']),
'category' => $recurring['category'],
'type' => $recurring['type'],
'priority' => $recurring['priority'],
'status' => 'Open'
];
// Create the ticket
$result = $ticketModel->createTicket($ticketData, $recurring['created_by']);
if ($result['success']) {
$ticketId = $result['ticket_id'];
logMessage("Created ticket: " . $ticketId);
// Assign to user if specified. assignTicket() requires a non-null
// "assigned_by"; fall back to the assignee when created_by is null
// (recurring schedules may have no creator).
if (!empty($recurring['assigned_to'])) {
$assignedBy = (int)($recurring['created_by'] ?? $recurring['assigned_to']);
$ticketModel->assignTicket($ticketId, (int)$recurring['assigned_to'], $assignedBy);
}
// Log to audit
$auditLog->log(
$recurring['created_by'],
'create',
'ticket',
$ticketId,
['source' => 'recurring', 'recurring_id' => $recurring['recurring_id']]
);
// Fire the same Matrix "ticket created" notification the manual and
// external-API create paths send, so recurring tickets aren't silent.
NotificationHelper::sendTicketNotification($ticketId, $ticketData, 'automated');
$created++;
} else {
logMessage("ERROR: Failed to create ticket - " . ($result['error'] ?? 'Unknown error'));
$errors++;
}
} catch (Exception $e) {
logMessage("ERROR: Exception processing recurring ticket - " . $e->getMessage());
$errors++;
}
}
// Ticket counts changed — invalidate the cached dashboard stats once for the
// whole run (mirrors the manual/API create paths, which invalidate per create).
if ($created > 0) {
(new StatsModel($conn))->invalidateCache();
}
logMessage("Completed: Created $created tickets, $errors errors");
Database::close();
} catch (Exception $e) {
logMessage("FATAL ERROR: " . $e->getMessage());
exit(1);
}
/**
* Process template variables
*/
function processTemplate($template)
{
if (empty($template)) {
return $template;
}
$replacements = [
'{{date}}' => date('Y-m-d'),
'{{time}}' => date('H:i:s'),
'{{datetime}}' => date('Y-m-d H:i:s'),
'{{week}}' => date('W'),
'{{month}}' => date('F'),
'{{year}}' => date('Y'),
'{{day_of_week}}' => date('l'),
'{{day}}' => date('d'),
];
return str_replace(array_keys($replacements), array_values($replacements), $template);
}
logMessage("Cron job finished");