313 lines
15 KiB
PHP
313 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* ==============================================================================
|
|
* SAQEL ENTERPRISE - STUDENT ACCESS CONTROL & GRADE PROGRESSION SERVICE
|
|
* ==============================================================================
|
|
*
|
|
* ملف: StudentAccessControlService.php
|
|
* الهدف المعماري:
|
|
* 1. حوكمة وصول الطالب للمحتوى بناءً على صفه الدراسي الحالي والمترفع له حصراً بواسطة الرقم الوطني.
|
|
* - إذا كان الطالب في الصف السادس، يُمنع تماماً من الوصول لحصص الصف الخامس (السابق) أو الصف السابع (اللاحق).
|
|
* 2. تطبيق النموذج المالي المزدوج:
|
|
* - طلبة المدارس الشريكة (مديرية الثقافة العسكرية أو المدارس الخاصة المسجلة): الوصول مجاني تماماً (0 دينار)
|
|
* عبر تصريح مدرسي مشمول (school_included).
|
|
* - الطلبة الخارجيون المستقلون: يلزمهم تصريح مدفوع عبر كليك (full_marketplace).
|
|
* 3. الترفيع السنوي الآلي (Annual Grade Promotion) وقفل السجلات القديمة.
|
|
*/
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Core\Database;
|
|
use App\Core\Security;
|
|
|
|
class StudentAccessControlService
|
|
{
|
|
/**
|
|
* التحقق الصارم من أهلية وصلاحية الطالب للوصول إلى درس معين
|
|
*
|
|
* @param int|null $studentId معرف الطالب في قاعدة البيانات
|
|
* @param string|null $nationalId الرقم الوطني الأردني
|
|
* @param string $targetGrade الصف الدراسي للدرس المطلوب (مثل grade_6, grade_10)
|
|
* @param int|null $courseId معرف الدورة
|
|
* @param int|null $lessonId معرف الدرس
|
|
* @return array [allowed => 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
|
|
): 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);
|
|
|
|
// 2. التحقق الجنائي الصارم من قفل الصف (Grade-Gate Restriction)
|
|
// الطالب لا يستطيع مشاهدة حصص صفوف سابقة ولا صفوف لاحقة
|
|
if ($activeStudentGrade !== $normalizedTargetGrade) {
|
|
$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 ($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) {
|
|
$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 ($g === '10' || $g === 'grade_10' || $g === 'grade10' || $g === 'عاشر' || $g === 'الصف العاشر') return 'grade_10';
|
|
if ($g === '9' || $g === 'grade_9' || $g === 'grade9' || $g === 'تاسع' || $g === 'الصف التاسع') return 'grade_9';
|
|
if ($g === '8' || $g === 'grade_8' || $g === 'grade8' || $g === 'ثامن' || $g === 'الصف الثامن') return 'grade_8';
|
|
if ($g === '7' || $g === 'grade_7' || $g === 'grade7' || $g === 'سابع' || $g === 'الصف السابع') return 'grade_7';
|
|
if ($g === '6' || $g === 'grade_6' || $g === 'grade6' || $g === 'سادس' || $g === 'الصف السادس') return 'grade_6';
|
|
if ($g === '5' || $g === 'grade_5' || $g === 'grade5' || $g === 'خامس' || $g === 'الصف الخامس') return 'grade_5';
|
|
if ($g === '11' || $g === 'grade_11' || $g === 'حادي عشر' || $g === 'أول ثانوي') return 'grade_11';
|
|
if ($g === '12' || $g === 'grade_12' || $g === 'tawjihi' || $g === 'tawjihi_2008' || $g === 'توجيهي') 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,
|
|
};
|
|
}
|
|
}
|