Update Saqel Platform: 2026-09-04 03:20:29
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
<?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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user