Update: 2026-08-07 15:50:04

This commit is contained in:
Hamza-Ayed
2026-08-07 15:50:04 +03:00
parent b2a95c53aa
commit 9338e76894
11 changed files with 1375 additions and 4 deletions
+212
View File
@@ -0,0 +1,212 @@
<?php
/**
* sms/helpers.php — أدوات مشتركة لمسار الطلب بلا إنترنت
* ─────────────────────────────────────────────────────────────
* ‏دوال صغيرة يستعملها الويب‌هوك والكرون و acceptRide معاً. وُضعت هنا
* ‏لا في connect.php لأن مسار SMS ليس جزءاً من المسار الحرج للتطبيقين،
* ‏فلا داعي لتحميله في كل طلب.
*/
// ═══════════════════════════════════════════════════════════════
// توحيد رقم الهاتف
//
// ‏الرقم يصل بصيغ لا حصر لها: 0791234567، +962791234567،
// ‏00962791234567، وبأرقام عربية-هندية أحياناً. بلا توحيد يصبح نفس
// ‏الراكب عدّة حسابات، ولن يجد الردّ طريقه إليه.
// ═══════════════════════════════════════════════════════════════
/** مقدمات الدول التي نعمل فيها، وطول الرقم المحلي بعد إسقاط الصفر. */
const SMS_COUNTRY_CODES = [
'Jordan' => '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;
}