'962', 'Egypt' => '20', 'Syria' => '963', ]; function smsNormalizePhone(string $raw): string { // ‏الأرقام العربية-الهندية (٠١٢…) تصل من بعض الهواتف كما هي. $raw = strtr($raw, [ '٠'=>'0','١'=>'1','٢'=>'2','٣'=>'3','٤'=>'4', '٥'=>'5','٦'=>'6','٧'=>'7','٨'=>'8','٩'=>'9', '۰'=>'0','۱'=>'1','۲'=>'2','۳'=>'3','۴'=>'4', '۵'=>'5','۶'=>'6','۷'=>'7','۸'=>'8','۹'=>'9', ]); $digits = preg_replace('/\D+/', '', $raw) ?? ''; if ($digits === '') return ''; $cc = SMS_COUNTRY_CODES[getenv('APP_COUNTRY') ?: 'Jordan'] ?? '962'; // 00962… → 962… if (str_starts_with($digits, '00')) { $digits = substr($digits, 2); } // ‏محلي يبدأ بصفر (0791234567) → نستبدل الصفر بمقدمة الدولة. if (str_starts_with($digits, '0')) { $digits = $cc . ltrim($digits, '0'); } elseif (!str_starts_with($digits, $cc)) { // ‏بلا صفر وبلا مقدمة (791234567) → نفترض محلياً. $digits = $cc . $digits; } return $digits; } // ═══════════════════════════════════════════════════════════════ // الصادر // ═══════════════════════════════════════════════════════════════ /** * ‏يضع رسالة في الطابور. لا يرسل — الإرسال مسؤولية الهاتف الذي يسحب * ‏من outbox_pull.php. الفصل مقصود: نداء شبكي فاشل داخل مسار قبول * ‏الرحلة كان سيؤخّر السائق من أجل رسالة نصية. */ function smsEnqueue(PDO $con, string $recipient, string $body, string $context = '', ?string $refId = null): bool { $to = smsNormalizePhone($recipient); if ($to === '' || trim($body) === '') return false; try { $con->prepare(" INSERT INTO sms_outbox (recipient, body, context, ref_id) VALUES (?, ?, ?, ?) ")->execute([$to, trim($body), $context ?: null, $refId]); return true; } catch (PDOException $e) { error_log('[sms] تعذّر وضع رسالة في الطابور: ' . $e->getMessage()); return false; } } // ═══════════════════════════════════════════════════════════════ // مطابقة المعالم // ═══════════════════════════════════════════════════════════════ /** جدول المعالم حسب السوق — الأسماء تاريخية ولا تتبع نمطاً واحداً. */ function smsPlacesTable(): string { return match (getenv('APP_COUNTRY') ?: 'Jordan') { 'Egypt' => 'placesEgypt', 'Syria' => 'palces11', default => 'places', }; } /** * ‏يجرّد النص العربي مما يعيق المطابقة: التشكيل، التطويل، واختلاف * ‏رسم الألف والياء والتاء المربوطة. «مستشفى البشير» و«مستشفي البشير» * ‏يكتبهما الناس بالتبادل، ولا يجوز أن يفشل الطلب لهذا السبب. */ function smsNormalizeArabic(string $s): string { $s = trim($s); $s = preg_replace('/[\x{064B}-\x{0652}\x{0640}]/u', '', $s) ?? $s; // تشكيل + تطويل $s = strtr($s, ['أ'=>'ا','إ'=>'ا','آ'=>'ا','ى'=>'ي','ة'=>'ه','ؤ'=>'و','ئ'=>'ي']); $s = preg_replace('/\s+/u', ' ', $s) ?? $s; return trim($s); } /** * ‏نفس تجريد smsNormalizeArabic لكن كتعبير SQL على عمود. * * ‏ضروري لا تجميلي: نحن نوحّد نص الراكب («المستشفى» ⇒ «المستشفي»)، * ‏فلو قارنّاه بالعمود الخام لما طابق شيئاً أبداً — الجدول يحمل الرسم * ‏الأصلي بالتاء المربوطة والألف المقصورة. * * ‏نقبل ثمنه: التعبير يمنع استخدام الفهرس. الجدول عشرات الآلاف من * ‏الصفوف والمسح كامله ميلي‌ثوانٍ، والقناة منخفضة التردد بطبعها. */ function smsSqlNormalize(string $column): string { $expr = "TRIM($column)"; foreach ([['أ','ا'], ['إ','ا'], ['آ','ا'], ['ى','ي'], ['ة','ه'], ['ؤ','و'], ['ئ','ي'], ['ـ','']] as [$from, $to]) { $expr = "REPLACE($expr, '$from', '$to')"; } return $expr; } /** * ‏يبحث عن معلم بالاسم. ثلاث محاولات بترتيب الثقة: تطابق تام، ثم * ‏«يبدأ بـ»، ثم «يحتوي». الترتيب مهم — البحث بـ LIKE '%…%' وحده * ‏يجعل «الجامعة» تطابق أول صف عشوائي من عشرات الجامعات. * * @return array{lat:float,lng:float,name:string}|null */ function smsFindPlace(PDO $con, string $query): ?array { $q = smsNormalizeArabic($query); if (mb_strlen($q) < 2) return null; $table = smsPlacesTable(); // ‏نبحث في الأعمدة الثلاثة: الاسم الأساسي والعربي والإنجليزي. // ‏ثلاث محاولات بترتيب تنازلي في الثقة، ونتوقف عند أول نجاح. $attempts = [ ['op' => '=', 'value' => $q], ['op' => 'LIKE', 'value' => $q . '%'], ['op' => 'LIKE', 'value' => '%' . $q . '%'], ]; $nName = smsSqlNormalize('name'); $nNameAr = smsSqlNormalize('name_ar'); $nNameEn = smsSqlNormalize('name_en'); foreach ($attempts as $a) { try { $stmt = $con->prepare(" SELECT latitude, longitude, COALESCE(NULLIF(name_ar,''), name) AS label FROM `$table` WHERE $nName {$a['op']} ? OR $nNameAr {$a['op']} ? OR $nNameEn {$a['op']} ? -- ‏الأقصر أولاً: «مستشفى البشير» تسبق «موقف باص مستشفى -- ‏البشير الخلفي» حين يكتب الراكب «البشير» فقط. ORDER BY CHAR_LENGTH(name) ASC LIMIT 1 "); // ‏معلَّمات موضعية لا مسمّاة: إعادة استخدام اسم واحد ثلاث // ‏مرات تفشل حين تكون المحاكاة معطّلة. $stmt->execute([$a['value'], $a['value'], $a['value']]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row && is_numeric($row['latitude']) && is_numeric($row['longitude'])) { return [ 'lat' => (float) $row['latitude'], 'lng' => (float) $row['longitude'], 'name' => (string) ($row['label'] ?: $query), ]; } } catch (PDOException $e) { error_log('[sms] فشل البحث عن معلم: ' . $e->getMessage()); return null; } } return null; } // ═══════════════════════════════════════════════════════════════ // المسافة والزمن // ═══════════════════════════════════════════════════════════════ /** * ‏مسافة هافرساين بالكيلومترات مضروبة بمعامل التواء الطرق. * * ‏لا نستدعي خدمة توجيه هنا عمداً: نداء خارجي داخل مسار الويب‌هوك يعني * ‏أن انقطاعه يوقف كل الطلبات، والتسعير النهائي يتم في add_ride على أي * ‏حال. هذا تقدير كافٍ لبدء الرحلة. */ function smsHaversineKm(float $lat1, float $lng1, float $lat2, float $lng2): float { $R = 6371.0; $dLat = deg2rad($lat2 - $lat1); $dLng = deg2rad($lng2 - $lng1); $a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2; // ‏1.3: الطريق ليس خطاً مستقيماً. المعامل تقريب متحفّظ شائع في // ‏المدن العربية ذات الشبكة غير المنتظمة. return $R * 2 * atan2(sqrt($a), sqrt(1 - $a)) * 1.3; }