Commit cd771856 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(cron): the scheduled-job subsystem has never run, and gate it before it does

cron/runner.php writes a cron_job_log row before every eligible job. That table
does not exist, so the runner threw on the first job with shouldRun() === true
and none of the 43 scheduled jobs has ever executed: subscription generation,
instalment default handling, activity-subscription revocation, academy
settlements, coach payroll, monthly depreciation, and every expiry reminder.

The container's crontab is present and cron is running — the hourly entry has
been firing into an immediate exception the whole time, which is why
storage/logs/cron.log does not exist.

Creating the table alone would be reckless the night before a finance review:
the crontab fires hourly, so all 43 would start on the next tick, and several
write off receivables, impose fines, drop memberships and auto-complete waivers
(which now post accrual entries). So the runner is additionally gated behind
system_config.cron_enabled, seeded to 0.

Turn it on from Settings when someone can watch the first run. Until then the
runner exits with a clear message rather than pretending to work.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 7313a539
...@@ -13,6 +13,22 @@ $app = \App\Core\App::getInstance(); ...@@ -13,6 +13,22 @@ $app = \App\Core\App::getInstance();
$app->boot(); $app->boot();
$db = $app->db(); $db = $app->db();
// ── Master switch ────────────────────────────────────────────────────────
// These 43 jobs have never run: the runner writes a cron_job_log row before each
// one and that table did not exist, so it threw on the first eligible job. With
// the table created they would all start firing on the next hourly tick — and
// several of them write off receivables, impose fines, drop memberships and
// auto-complete waivers. That is not something to switch on unattended.
//
// Ships OFF. Enable from Settings (cron_enabled = 1) when someone is available to
// watch the first run, ideally starting with a quiet period.
$flag = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = 'cron_enabled'");
if ($flag !== null && (string) $flag['config_value'] !== '1') {
echo "[" . date('Y-m-d H:i:s') . "] Cron is disabled (system_config.cron_enabled = 0). Nothing run.\n";
return;
}
$jobFiles = glob($basePath . '/cron/jobs/*.php'); $jobFiles = glob($basePath . '/cron/jobs/*.php');
echo "[" . date('Y-m-d H:i:s') . "] Cron runner started. Found " . count($jobFiles) . " jobs.\n"; echo "[" . date('Y-m-d H:i:s') . "] Cron runner started. Found " . count($jobFiles) . " jobs.\n";
......
<?php
declare(strict_types=1);
/**
* Create the table the cron runner writes to before every job.
*
* cron/runner.php inserts a `cron_job_log` row the moment a job reports
* shouldRun() === true. The table has never existed, so the runner threw on the
* first eligible job and every one of the 43 scheduled jobs has never executed —
* subscription generation, instalment default handling, activity revocation,
* academy settlements, coach payroll, depreciation, every expiry reminder.
*
* Creating the table is only half the fix. The container's crontab already fires
* hourly, so the moment this exists those 43 jobs begin running — several of them
* write off receivables, impose fines, drop memberships and auto-complete waivers.
* Switching all of that on unattended is not something to do by accident, so the
* runner is additionally gated behind a `cron_enabled` flag that ships OFF. Turn it
* on deliberately from Settings once someone is watching the first run.
*/
return function (\App\Core\Database $db): void {
$db->raw("
CREATE TABLE IF NOT EXISTS `cron_job_log` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`job_name` VARCHAR(100) NOT NULL,
`started_at` DATETIME NOT NULL,
`finished_at` DATETIME NULL,
`status` ENUM('running','completed','failed') NOT NULL DEFAULT 'running',
`records_processed` INT UNSIGNED NOT NULL DEFAULT 0,
`execution_time_ms` INT UNSIGNED NULL,
`error_message` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_cron_job_name` (`job_name`, `started_at`),
INDEX `idx_cron_status` (`status`, `started_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
};
<?php
declare(strict_types=1);
/**
* Ship the cron master switch in the OFF position.
*
* See Phase_102_001_create_cron_job_log — the 43 scheduled jobs have never run,
* and creating the log table alone would start them all on the next hourly tick.
* Several change money or membership state without a human present.
*
* Enable deliberately from Settings once someone can watch the first run.
*/
return function (\App\Core\Database $db): void {
$existing = $db->selectOne(
"SELECT id FROM system_config WHERE config_key = 'cron_enabled'"
);
if ($existing) {
return;
}
$db->insert('system_config', [
'config_key' => 'cron_enabled',
'config_value' => '0',
'config_type' => 'boolean',
'group_name' => 'system',
'description_ar' => 'تشغيل المهام المجدولة (الكرون). مغلق افتراضيًا — ٤٣ مهمة لم تعمل من قبل، وبعضها يسقط مديونيات ويوقّع غرامات ويغيّر حالة العضويات. فعّله وأنت مراقب لأول دورة.',
'description_en' => 'Master switch for scheduled jobs. Ships OFF — 43 jobs have never run and some write off receivables, impose fines and change membership state. Enable while someone is watching the first run.',
'is_editable' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment