Files
tripz-llc/backend/sms/helpers.php
T
2026-08-09 16:56:13 +03:00

213 lines
10 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}