213 lines
10 KiB
PHP
213 lines
10 KiB
PHP
<?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;
|
||
}
|