Harden published curriculum and student flows
This commit is contained in:
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* توحيد أسماء ومفاتيح الصفوف
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user