Harden published curriculum and student flows

This commit is contained in:
Hamza-Ayed
2026-09-30 08:22:17 +03:00
parent e8163fe265
commit ce7b0fcf14
53 changed files with 4723 additions and 3317 deletions
@@ -38,7 +38,7 @@ class StudentAccessControlService
string $targetGrade,
?int $courseId = null,
?int $lessonId = null,
bool $allowSideEffects = true
bool $allowSideEffects = false
): array {
// 1. استرجاع بيانات الطالب وسجله الدراسي
$student = null;
@@ -64,19 +64,9 @@ class StudentAccessControlService
);
}
// إذا لم يكن مسجلاً، نتحقق مما إذا كان الدرس معاينة مجانية (Preview)
// Protected content requires a current student record. A preview flag
// must not turn a missing/deleted student into a valid entitlement.
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',
@@ -86,79 +76,71 @@ class StudentAccessControlService
];
}
$activeStudentGrade = self::normalizeGrade($student['grade_level'] ?? 'grade_10');
$activeStudentGrade = self::normalizeGrade((string)($student['grade_level'] ?? ''));
$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'])) {
if ($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
'allowed' => false, 'reason' => 'grade_mismatch',
'message' => 'الدرس خارج صف الطالب المسجل؛ غيّر صفك من الملف المصرح أو راجع المدرسة.',
'student_grade' => $activeStudentGrade, 'target_grade' => $normalizedTargetGrade,
'is_sponsored' => (bool)($student['is_school_sponsored'] ?? false),
];
}
// ب. إذا كان طالباً مستقلاً خارج المدارس الشريكة (External Student)
// A lesson read never changes grade or creates a school access pass.
// Entitlements are checked against an actual course and school roster.
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) {
$course = Database::selectOne(
'SELECT c.id, c.school_id, c.is_school_exclusive, c.is_published, c.price_jod,
c.grade_level, t.is_school_exclusive AS teacher_school_exclusive,
t.is_marketplace_public
FROM courses c JOIN teachers t ON t.id=c.teacher_id WHERE c.id=? LIMIT 1',
[$courseId]
);
if (!$course || (int)$course['is_published'] !== 1) {
return ['allowed' => false, 'reason' => 'course_unavailable', 'student_grade' => $activeStudentGrade];
}
if ($course['grade_level'] !== null && $course['grade_level'] !== ''
&& self::normalizeGrade((string)$course['grade_level']) !== $normalizedTargetGrade) {
return ['allowed' => false, 'reason' => 'course_grade_mismatch', 'student_grade' => $activeStudentGrade];
}
if ($lessonId) {
$sourceLesson = Database::selectOne('SELECT course_id FROM lessons WHERE id=? LIMIT 1', [$lessonId]);
if (!$sourceLesson || (int)$sourceLesson['course_id'] !== $courseId) {
return ['allowed' => false, 'reason' => 'lesson_course_mismatch', 'student_grade' => $activeStudentGrade];
}
}
$schoolId = (int)($course['school_id'] ?? 0);
if ($schoolId > 0 || (int)$course['is_school_exclusive'] === 1 || (int)$course['teacher_school_exclusive'] === 1) {
if ($schoolId <= 0 || (int)($student['school_id'] ?? 0) !== $schoolId
|| empty($student['is_school_sponsored'])) {
return ['allowed' => false, 'reason' => 'school_scope_mismatch', 'student_grade' => $activeStudentGrade];
}
$roster = Database::selectOne(
'SELECT grade_level FROM school_rosters WHERE school_id=? AND claimed_student_id=? AND is_claimed=1 LIMIT 1',
[$schoolId, (int)$student['id']]
);
if (!$roster || self::normalizeGrade((string)$roster['grade_level']) !== $normalizedTargetGrade) {
return ['allowed' => false, 'reason' => 'school_roster_unverified', 'student_grade' => $activeStudentGrade];
}
return [
'allowed' => true,
'reason' => 'free_course',
'student_grade' => $activeStudentGrade,
'is_sponsored' => false
'allowed' => true, 'reason' => 'school_course_included',
'student_grade' => $activeStudentGrade, 'is_sponsored' => true,
];
}
if ((int)$course['is_marketplace_public'] !== 1) {
return ['allowed' => false, 'reason' => 'not_marketplace_public', 'student_grade' => $activeStudentGrade];
}
if ((float)$course['price_jod'] <= 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 pass_type IN ('discounted_micro_pass','full_marketplace')
AND (expires_at IS NULL OR expires_at > NOW())
LIMIT 1",
[(int)$student['id'], $courseId]
@@ -174,7 +156,7 @@ class StudentAccessControlService
];
}
// فحص إذا كان الدرس معاينة مجانية
// An explicit free preview is allowed only on a public course.
if ($lessonId) {
$lesson = Database::selectOne("SELECT is_free_preview FROM lessons WHERE id = ? LIMIT 1", [$lessonId]);
if ($lesson && !empty($lesson['is_free_preview'])) {
@@ -279,30 +261,6 @@ class StudentAccessControlService
];
}
/**
* التحقق من وجود تصريح مدرسي مجاني أو إنشاؤه تلقائياً لطلبة المدارس الشريكة
*/
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]
);
}
}
/**
* توحيد أسماء ومفاتيح الصفوف
*/