bool, reason => string, student_grade => string, is_sponsored => bool] */ public static function validateLessonAccess( ?int $studentId, ?string $nationalId, string $targetGrade, ?int $courseId = null, ?int $lessonId = null, bool $allowSideEffects = true ): array { // 1. استرجاع بيانات الطالب وسجله الدراسي $student = null; if ($studentId) { $student = Database::selectOne( "SELECT s.*, sch.name as school_name, sch.type as school_type, sch.directorate_id, d.type as directorate_type FROM students s LEFT JOIN schools sch ON s.school_id = sch.id LEFT JOIN directorates d ON sch.directorate_id = d.id WHERE s.id = ? LIMIT 1", [$studentId] ); } elseif (!empty($nationalId)) { $student = Database::selectOne( "SELECT s.*, sch.name as school_name, sch.type as school_type, sch.directorate_id, d.type as directorate_type FROM students s LEFT JOIN schools sch ON s.school_id = sch.id LEFT JOIN directorates d ON sch.directorate_id = d.id WHERE s.national_id = ? LIMIT 1", [$nationalId] ); } // إذا لم يكن مسجلاً، نتحقق مما إذا كان الدرس معاينة مجانية (Preview) if (!$student) { if ($lessonId) { $lesson = Database::selectOne("SELECT is_free_preview FROM lessons WHERE id = ? LIMIT 1", [$lessonId]); if ($lesson && !empty($lesson['is_free_preview'])) { return [ 'allowed' => true, 'reason' => 'free_preview', 'student_grade' => 'guest', 'is_sponsored' => false ]; } } return [ 'allowed' => false, 'reason' => 'unauthenticated_or_not_found', 'message' => 'يرجى تسجيل الدخول أو إدخال الرقم الوطني للتحقق من الصلاحية الدراسية.', 'student_grade' => 'unknown', 'is_sponsored' => false ]; } $activeStudentGrade = self::normalizeGrade($student['grade_level'] ?? 'grade_10'); $normalizedTargetGrade = self::normalizeGrade($targetGrade); // Auto-heal obsolete database schema default: // If student was recorded with the old default 'tawjihi_2008' or empty and target is grade_10 if ($allowSideEffects && ($student['grade_level'] === 'tawjihi_2008' || empty($student['grade_level'])) && $normalizedTargetGrade === 'grade_10') { Database::query("UPDATE students SET grade_level = 'grade_10', updated_at = NOW() WHERE id = ?", [(int)$student['id']]); $student['grade_level'] = 'grade_10'; $activeStudentGrade = 'grade_10'; } // 2. التحقق من قفل الصف الدراسي: // يطبق قفل الصف الصارم حصراً على طلبة المدارس الشريكة المشمولة (الثقافة العسكرية والمدارس المرتبطة بمديريات) // أما الطلبة المستقلون وحسابات التجربة والتعلم الحر، فيتم تحديث صفهم النشط تلقائياً وفق المحتوى المختار $isCohortLocked = !empty($student['is_school_sponsored']) || !empty($student['school_id']); if ($activeStudentGrade !== $normalizedTargetGrade) { if (!$isCohortLocked && !empty($student['id']) && $allowSideEffects) { Database::query("UPDATE students SET grade_level = ? WHERE id = ?", [$normalizedTargetGrade, (int)$student['id']]); $activeStudentGrade = $normalizedTargetGrade; } else { $targetGradeName = self::getGradeDisplayName($normalizedTargetGrade); $currentGradeName = self::getGradeDisplayName($activeStudentGrade); return [ 'allowed' => false, 'reason' => 'grade_mismatch', 'message' => "غير مصرح: أنت مسجل حالياً في ({$currentGradeName})، ولا يمكنك فتح حصص ({$targetGradeName}) حفاظاً على التركيز والمسار الأكاديمي المعتمد.", 'student_grade' => $activeStudentGrade, 'target_grade' => $normalizedTargetGrade, 'is_sponsored' => (bool)($student['is_school_sponsored'] ?? false) ]; } } // 3. التحقق من النموذج المالي المزدوج: // أ. إذا كان الطالب تابعاً لمدارس الثقافة العسكرية أو مدرسة خاصة شريكة $isMilitaryCulture = ($student['directorate_type'] ?? '') === 'military_culture' || ($student['school_type'] ?? '') === 'military_culture'; $isPrivateSponsored = !empty($student['is_school_sponsored']) && !empty($student['school_id']); if ($isMilitaryCulture || $isPrivateSponsored) { // الطالب مشمول مجاناً 100% (صفر دينار) if ($allowSideEffects && $courseId && !empty($student['id'])) { self::ensureSchoolIncludedPass((int)$student['id'], $courseId); } return [ 'allowed' => true, 'reason' => 'school_sponsored_free', 'message' => 'مشمول مجاناً عبر المنظومة المؤسسية الشريكة (مديرية الثقافة العسكرية / المدرسة المعتمدة).', 'student_grade' => $activeStudentGrade, 'is_sponsored' => true, 'fee_jod' => 0.00 ]; } // ب. إذا كان طالباً مستقلاً خارج المدارس الشريكة (External Student) if ($courseId) { // المساقات المجانية (السعر 0.00 دينار) متاحة فوراً ومجاناً للجميع $course = Database::selectOne("SELECT price_jod FROM courses WHERE id = ? LIMIT 1", [$courseId]); if ($course && (float)($course['price_jod'] ?? 0) <= 0.0) { return [ 'allowed' => true, 'reason' => 'free_course', 'student_grade' => $activeStudentGrade, 'is_sponsored' => false ]; } $activePass = Database::selectOne( "SELECT id, pass_type, expires_at, is_active FROM course_access_passes WHERE student_id = ? AND course_id = ? AND is_active = 1 AND (expires_at IS NULL OR expires_at > NOW()) LIMIT 1", [(int)$student['id'], $courseId] ); if ($activePass) { return [ 'allowed' => true, 'reason' => 'paid_pass_active', 'student_grade' => $activeStudentGrade, 'is_sponsored' => false, 'pass_id' => $activePass['id'] ]; } // فحص إذا كان الدرس معاينة مجانية if ($lessonId) { $lesson = Database::selectOne("SELECT is_free_preview FROM lessons WHERE id = ? LIMIT 1", [$lessonId]); if ($lesson && !empty($lesson['is_free_preview'])) { return [ 'allowed' => true, 'reason' => 'free_preview', 'student_grade' => $activeStudentGrade, 'is_sponsored' => false ]; } } return [ 'allowed' => false, 'reason' => 'payment_required', 'message' => 'يتطلب هذا المساق تفعيل الاشتراك عبر نظام الدفع الفوري (CliQ) للطلبة المستقلين خارج المدارس الشريكة.', 'student_grade' => $activeStudentGrade, 'is_sponsored' => false, 'course_id' => $courseId ]; } // افتراضياً لدروس المنهاج المعتمد لنفس الصف return [ 'allowed' => true, 'reason' => 'curriculum_default', 'student_grade' => $activeStudentGrade, 'is_sponsored' => false ]; } /** * ترفيع الطالب سنوياً إلى الصف اللاحق (مثال: من خامس إلى سادس) * وإلغاء صلاحية الوصول لحصص الصفوف السابقة */ public static function promoteStudent(int $studentId, string $newGradeLevel): array { $student = Database::selectOne("SELECT id, full_name, grade_level, national_id FROM students WHERE id = ? LIMIT 1", [$studentId]); if (!$student) { return ['status' => 'error', 'message' => 'الطالب غير موجود']; } $oldGrade = self::normalizeGrade($student['grade_level']); $promotedGrade = self::normalizeGrade($newGradeLevel); Database::query("UPDATE students SET grade_level = ?, updated_at = NOW() WHERE id = ?", [$promotedGrade, $studentId]); Database::query("UPDATE school_rosters SET grade_level = ? WHERE national_id = ?", [$promotedGrade, $student['national_id']]); // إلغاء تفعيل التصاريح السابقة الخاصة بالصف المنتهي Database::query( "UPDATE course_access_passes cap JOIN courses c ON cap.course_id = c.id SET cap.is_active = 0 WHERE cap.student_id = ? AND c.grade_level = ?", [$studentId, $oldGrade] ); $oldName = self::getGradeDisplayName($oldGrade); $newName = self::getGradeDisplayName($promotedGrade); return [ 'status' => 'success', 'student_id' => $studentId, 'student_name' => $student['full_name'], 'old_grade' => $oldGrade, 'promoted_grade' => $promotedGrade, 'message' => "تم ترفيع الطالب ({$student['full_name']}) بنجاح من ({$oldName}) إلى ({$newName}) وتحديث الصلاحيات." ]; } /** * ترفيع كشف المدرسة بالكامل دفعة واحدة (Batch Grade Promotion) */ public static function batchPromoteRoster(int $schoolId, string $fromGrade, string $toGrade): array { $from = self::normalizeGrade($fromGrade); $to = self::normalizeGrade($toGrade); $count = (int)Database::selectOne( "SELECT COUNT(*) as cnt FROM students WHERE school_id = ? AND grade_level = ?", [$schoolId, $from] )['cnt']; Database::query( "UPDATE students SET grade_level = ?, updated_at = NOW() WHERE school_id = ? AND grade_level = ?", [$to, $schoolId, $from] ); Database::query( "UPDATE school_rosters SET grade_level = ? WHERE school_id = ? AND grade_level = ?", [$to, $schoolId, $from] ); return [ 'status' => 'success', 'school_id' => $schoolId, 'from_grade' => $from, 'to_grade' => $to, 'promoted_count' => $count, 'message' => "تم ترفيع {$count} طالباً في المدرسة بنجاح وتحديث صلاحياتهم الأكاديمية." ]; } /** * التحقق من وجود تصريح مدرسي مجاني أو إنشاؤه تلقائياً لطلبة المدارس الشريكة */ private static function ensureSchoolIncludedPass(int $studentId, int $courseId): void { $existing = Database::selectOne( "SELECT id FROM course_access_passes WHERE student_id = ? AND course_id = ? LIMIT 1", [$studentId, $courseId] ); if (!$existing) { $course = Database::selectOne("SELECT teacher_id FROM courses WHERE id = ? LIMIT 1", [$courseId]); $teacherId = (int)($course['teacher_id'] ?? 1); Database::insert( "INSERT INTO course_access_passes (student_id, course_id, teacher_id, pass_type, price_paid_jod, expires_at, is_active) VALUES (?, ?, ?, 'school_included', 0.00, DATE_ADD(NOW(), INTERVAL 1 YEAR), 1)", [$studentId, $courseId, $teacherId] ); } } /** * توحيد أسماء ومفاتيح الصفوف */ public static function normalizeGrade(string $grade): string { $g = strtolower(trim($grade)); if (str_contains($g, 'عاشر') || str_contains($g, 'grade_10') || str_contains($g, 'grade10') || str_contains($g, 'grade 10') || $g === '10') return 'grade_10'; if (str_contains($g, 'تاسع') || str_contains($g, 'grade_9') || str_contains($g, 'grade9') || str_contains($g, 'grade 9') || $g === '9') return 'grade_9'; if (str_contains($g, 'ثامن') || str_contains($g, 'grade_8') || str_contains($g, 'grade8') || str_contains($g, 'grade 8') || $g === '8') return 'grade_8'; if (str_contains($g, 'سابع') || str_contains($g, 'grade_7') || str_contains($g, 'grade7') || str_contains($g, 'grade 7') || $g === '7') return 'grade_7'; if (str_contains($g, 'سادس') || str_contains($g, 'grade_6') || str_contains($g, 'grade6') || str_contains($g, 'grade 6') || $g === '6') return 'grade_6'; if (str_contains($g, 'خامس') || str_contains($g, 'grade_5') || str_contains($g, 'grade5') || str_contains($g, 'grade 5') || $g === '5') return 'grade_5'; if (str_contains($g, 'حادي') || str_contains($g, 'أول ثانوي') || str_contains($g, 'grade_11') || str_contains($g, 'grade 11') || $g === '11') return 'grade_11'; if (str_contains($g, 'ثاني عشر') || str_contains($g, 'توجيهي') || str_contains($g, 'tawjihi') || str_contains($g, 'grade_12') || str_contains($g, 'grade 12') || $g === '12') return 'grade_12'; return $g; } /** * اسم العرض للصف بالعربية */ public static function getGradeDisplayName(string $gradeKey): string { return match ($gradeKey) { 'grade_5' => 'الصف الخامس الأساسي', 'grade_6' => 'الصف السادس الأساسي', 'grade_7' => 'الصف السابع الأساسي', 'grade_8' => 'الصف الثامن الأساسي', 'grade_9' => 'الصف التاسع الأساسي', 'grade_10' => 'الصف العاشر الأساسي', 'grade_11' => 'الصف الحادي عشر (الأول ثانوي)', 'grade_12', 'tawjihi_2008' => 'الصف الثاني عشر (التوجيهي الوزاري)', default => $gradeKey, }; } }