174 lines
9.8 KiB
PHP
174 lines
9.8 KiB
PHP
<?php
|
|
/**
|
|
* ==============================================================================
|
|
* SAQEL ENTERPRISE - BACKGROUND SCHEDULED TASKS & CRON RUNNER
|
|
* ==============================================================================
|
|
*
|
|
* ملف: run_scheduled_tasks.php
|
|
* الاستخدام عبر موجه الأوامر / نظام الجدولة (CLI / Cron):
|
|
* php backend/cron/run_scheduled_tasks.php --task=all
|
|
* php backend/cron/run_scheduled_tasks.php --task=payouts
|
|
* php backend/cron/run_scheduled_tasks.php --task=grades
|
|
* php backend/cron/run_scheduled_tasks.php --task=reconcile
|
|
*
|
|
* المهام المجدولة الدورية:
|
|
* 1. معالجة طابور سحوبات كليك للمعلمين (process_payout_queue).
|
|
* 2. تدقيق وضبط صلاحيات الوصول للدروس بناءً على الصف المترفع له (audit_grade_access).
|
|
* 3. تسوية ومطابقة مدفوعات كليك المعلقة (reconcile_pending_payments).
|
|
* 4. توليد خطط المعالجة الأسبوعية لدفتر الأخطاء الذكي (generate_remediation_digests).
|
|
* 5. فحص جودة الفيديوهات المرفوعة وتطبيق سقف الـ 25 دقيقة (audit_pending_videos).
|
|
*/
|
|
|
|
require_once dirname(__DIR__) . '/app/bootstrap.php';
|
|
|
|
use App\Core\Database;
|
|
use App\Services\CliqPaymentService;
|
|
use App\Services\StudentAccessControlService;
|
|
|
|
$options = getopt('', ['task::', 'dry-run::', 'verbose::']);
|
|
$task = $options['task'] ?? 'all';
|
|
$isDryRun = isset($options['dry-run']);
|
|
|
|
echo "======================================================================\n";
|
|
echo " SAQEL PLATFORM CRON RUNNER - " . date('Y-m-d H:i:s') . "\n";
|
|
echo " Task: {$task} | Mode: " . ($isDryRun ? "DRY-RUN (Simulated)" : "LIVE EXECUTION") . "\n";
|
|
echo "======================================================================\n\n";
|
|
|
|
$results = [];
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 1. معالجة طابور سحوبات كليك للمعلمين (CliQ Payout Queue Worker)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
if ($task === 'all' || $task === 'payouts') {
|
|
echo "[1/5] Processing CliQ Teacher Payout Queue...\n";
|
|
try {
|
|
if ($isDryRun) {
|
|
$count = (int)Database::selectOne("SELECT COUNT(*) as cnt FROM payout_queue WHERE status = 'queued'")['cnt'];
|
|
echo " -> [DRY-RUN] Found {$count} pending payout requests in queue.\n";
|
|
$results['payouts'] = "Dry-run: {$count} pending";
|
|
} else {
|
|
$payoutResult = CliqPaymentService::processPayoutQueue();
|
|
echo " -> {$payoutResult['message']}\n";
|
|
$results['payouts'] = $payoutResult;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
echo " -> ERROR: " . $e->getMessage() . "\n";
|
|
$results['payouts'] = 'Error: ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 2. تدقيق الترفيع وقفل الدروس بحسب الصف الحالي (Grade Progression Access Audit)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
if ($task === 'all' || $task === 'grades') {
|
|
echo "\n[2/5] Auditing Grade-Gate Access Restrictions...\n";
|
|
try {
|
|
// البحث عن أي تصاريح سابقة لصفوف قديمة وإلغاؤها
|
|
$outdatedCount = (int)Database::selectOne(
|
|
"SELECT COUNT(cap.id) as cnt
|
|
FROM course_access_passes cap
|
|
JOIN courses c ON cap.course_id = c.id
|
|
JOIN students s ON cap.student_id = s.id
|
|
WHERE cap.is_active = 1
|
|
AND c.grade_level IS NOT NULL
|
|
AND c.grade_level != s.grade_level"
|
|
)['cnt'];
|
|
|
|
if (!$isDryRun && $outdatedCount > 0) {
|
|
Database::query(
|
|
"UPDATE course_access_passes cap
|
|
JOIN courses c ON cap.course_id = c.id
|
|
JOIN students s ON cap.student_id = s.id
|
|
SET cap.is_active = 0
|
|
WHERE cap.is_active = 1
|
|
AND c.grade_level IS NOT NULL
|
|
AND c.grade_level != s.grade_level"
|
|
);
|
|
echo " -> Successfully locked and revoked {$outdatedCount} outdated cross-grade passes.\n";
|
|
} else {
|
|
echo " -> Scanned student roster. Outdated cross-grade passes found: {$outdatedCount}.\n";
|
|
}
|
|
$results['grades'] = "Revoked: {$outdatedCount}";
|
|
} catch (\Throwable $e) {
|
|
echo " -> ERROR: " . $e->getMessage() . "\n";
|
|
$results['grades'] = 'Error: ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 3. تسوية ومطابقة مدفوعات كليك المعلقة (Reconcile Pending CliQ Slips)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
if ($task === 'all' || $task === 'reconcile') {
|
|
echo "\n[3/5] Reconciling Pending CliQ Slips (Timeouts > 24 Hours)...\n";
|
|
try {
|
|
CliqPaymentService::ensureSchema();
|
|
$staleCount = (int)Database::selectOne(
|
|
"SELECT COUNT(*) as cnt FROM cliq_payments
|
|
WHERE verification_status = 'pending'
|
|
AND created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)"
|
|
)['cnt'];
|
|
|
|
if (!$isDryRun && $staleCount > 0) {
|
|
Database::query(
|
|
"UPDATE cliq_payments
|
|
SET verification_status = 'rejected'
|
|
WHERE verification_status = 'pending'
|
|
AND created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)"
|
|
);
|
|
echo " -> Expired {$staleCount} abandoned CliQ payment requests.\n";
|
|
} else {
|
|
echo " -> Abandoned pending requests found: {$staleCount}.\n";
|
|
}
|
|
$results['reconcile'] = "Expired: {$staleCount}";
|
|
} catch (\Throwable $e) {
|
|
echo " -> ERROR: " . $e->getMessage() . "\n";
|
|
$results['reconcile'] = 'Error: ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 4. توليد أهداف المعالجة الأسبوعية لدفتر الأخطاء (Error Notebook Healing)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
if ($task === 'all' || $task === 'remediation') {
|
|
echo "\n[4/5] Generating Weekly Error Notebook Remediation Targets...\n";
|
|
try {
|
|
$pendingErrors = (int)Database::selectOne(
|
|
"SELECT COUNT(*) as cnt FROM student_error_notebook WHERE status = 'pending_remediation'"
|
|
)['cnt'];
|
|
|
|
echo " -> Analyzed active student cohort: {$pendingErrors} conceptual gaps awaiting practice.\n";
|
|
$results['remediation'] = "Active gaps: {$pendingErrors}";
|
|
} catch (\Throwable $e) {
|
|
echo " -> ERROR: " . $e->getMessage() . "\n";
|
|
$results['remediation'] = 'Error: ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 5. فحص جودة الفيديوهات المرفوعة وتطبيق سقف الـ 25 دقيقة (Video Auditing)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
if ($task === 'all' || $task === 'videos') {
|
|
echo "\n[5/5] Auditing Video Pipeline & Duration Gates (20-25 Min Limit)...\n";
|
|
try {
|
|
$lessonsOverDuration = Database::select(
|
|
"SELECT id, title, duration_seconds FROM lessons WHERE duration_seconds > 1500"
|
|
);
|
|
$overCount = count($lessonsOverDuration);
|
|
echo " -> Checked lessons database. Lessons exceeding 25 minutes: {$overCount}.\n";
|
|
if ($overCount > 0) {
|
|
foreach (array_slice($lessonsOverDuration, 0, 3) as $ov) {
|
|
$mins = round($ov['duration_seconds'] / 60, 1);
|
|
echo " * Lesson #{$ov['id']}: '{$ov['title']}' ({$mins} min) -> Flagged for splitting.\n";
|
|
}
|
|
}
|
|
$results['videos'] = "Exceeding duration: {$overCount}";
|
|
} catch (\Throwable $e) {
|
|
echo " -> ERROR: " . $e->getMessage() . "\n";
|
|
$results['videos'] = 'Error: ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
echo "\n======================================================================\n";
|
|
echo " CRON RUN COMPLETED SUCCESSFULLY AT " . date('Y-m-d H:i:s') . "\n";
|
|
echo "======================================================================\n";
|