99 lines
4.4 KiB
PHP
99 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Core\Database;
|
|
use App\Core\Security;
|
|
|
|
/**
|
|
* ==============================================================================
|
|
* SAQEL ENTERPRISE (EDTECH 2.0) - SCHOOL ROSTER & NATIONAL ID ENCRYPTION SERVICE
|
|
* ==============================================================================
|
|
*
|
|
* ملف: SchoolRosterService.php
|
|
* الهدف المعماري:
|
|
* استيراد كشوفات المدارس وتشفير الأرقام الوطنية للطلبة والمعلمين (AES-256-GCM):
|
|
* 1. التحقق من صحة الرقم الوطني الأردني (10 خانات رقمية).
|
|
* 2. تشفير الرقم الوطني تشفيراً سيادياً وتوليد المؤشر الأعمى (Blind Index) لمنع تداخل الأسماء.
|
|
* 3. استيراد كشف المدرسة الجماعي وربط الطلبة بمدارسهم ومديريتهم.
|
|
*/
|
|
class SchoolRosterService
|
|
{
|
|
/**
|
|
* استيراد وتشفير كشف الطلبة للمدرسة
|
|
*/
|
|
public static function importStudentRoster(int $schoolId, array $records): array
|
|
{
|
|
$importedCount = 0;
|
|
$failedCount = 0;
|
|
$errors = [];
|
|
|
|
foreach ($records as $index => $row) {
|
|
$nationalId = trim((string)($row['national_id'] ?? ''));
|
|
$fullName = trim((string)($row['full_name'] ?? ''));
|
|
$gradeLevel = trim((string)($row['grade_level'] ?? 'grade_10'));
|
|
$stream = self::normalizeStream((string)($row['stream'] ?? 'general'));
|
|
|
|
// 1. Validate 10-digit Jordanian National ID
|
|
if (!preg_match('/^[0-9]{10}$/', $nationalId)) {
|
|
$failedCount++;
|
|
$errors[] = "السطر " . ($index + 1) . ": الرقم الوطني ($nationalId) غير صالح (يجب أن يتكون من 10 أرقام).";
|
|
continue;
|
|
}
|
|
|
|
if (empty($fullName)) {
|
|
$failedCount++;
|
|
$errors[] = "السطر " . ($index + 1) . ": اسم الطالب مطلوب.";
|
|
continue;
|
|
}
|
|
|
|
// 2. Encrypt National ID and generate HMAC Blind Index
|
|
$encryptedNationalId = Security::encrypt($nationalId);
|
|
$blindIndex = Security::blindIndex($nationalId);
|
|
|
|
$uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
|
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
|
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
|
);
|
|
|
|
// Insert or update in DB
|
|
try {
|
|
// If students table is active
|
|
Database::query(
|
|
"INSERT INTO students (uuid, national_id, national_id_hash, full_name, grade_level, stream, school_id, is_school_sponsored, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1, NOW())
|
|
ON DUPLICATE KEY UPDATE national_id = VALUES(national_id), full_name = VALUES(full_name), grade_level = VALUES(grade_level), stream = VALUES(stream), school_id = VALUES(school_id)",
|
|
[$uuid, $encryptedNationalId, $blindIndex, $fullName, $gradeLevel, $stream, $schoolId]
|
|
);
|
|
$importedCount++;
|
|
} catch (\Throwable $e) {
|
|
$failedCount++;
|
|
$errors[] = 'السطر ' . ($index + 1) . ': تعذر حفظ السجل في قاعدة البيانات.';
|
|
error_log('[SchoolRosterService] import failure: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return [
|
|
'status' => 'success',
|
|
'school_id' => $schoolId,
|
|
'total_received' => count($records),
|
|
'imported_count' => $importedCount,
|
|
'failed_count' => $failedCount,
|
|
'errors' => $errors,
|
|
'encryption_info'=> 'تم تشفير جميع الأرقام الوطنية بنجاح عبر خوارزمية AES-256-GCM السيادية ومؤشر HMAC الأعمى.',
|
|
];
|
|
}
|
|
|
|
private static function normalizeStream(string $stream): string
|
|
{
|
|
return match (trim(mb_strtolower($stream))) {
|
|
'علمي', 'scientific' => 'scientific',
|
|
'أدبي', 'literary' => 'literary',
|
|
'مهني', 'vocational' => 'vocational',
|
|
default => 'general',
|
|
};
|
|
}
|
|
|
|
}
|