Files
tripz-llc/backend/core/Services/LocationIntelligenceEngine.php
Hamza-AyedandClaude Opus 5 4d8414c96b feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 05:14:13 +03:00

164 lines
7.1 KiB
PHP

<?php
/**
* LocationIntelligenceEngine.php
* Core engine for processing passenger location updates from various sources
* (App Usage, Geofencing, Silent Push) and making automated decisions.
*/
class LocationIntelligenceEngine {
private $db;
public function __construct($dbConnection) {
$this->db = $dbConnection;
}
/**
* Process a location update from any source.
*
* @param int|string $passengerId
* @param float $lat
* @param float $lng
* @param string $source Enum: 'app_usage', 'geofence', 'silent_push'
* @return array Optional new geofence regions to register on user's device
*/
public function processLocationUpdate($passengerId, $lat, $lng, $source = 'app_usage', $batteryLevel = null) {
if (!function_exists('sendFCM_Internal')) {
require_once __DIR__ . '/../../functions.php';
}
// 1. Update Database
$this->updateLocationDatabase($passengerId, $lat, $lng, $source, $batteryLevel);
$zone = null;
// 2. Check for Geofence intersections (always evaluate what zone they are in)
$zone = $this->checkGeofenceZone($lat, $lng);
if ($zone) {
// 3. Trigger Campaigns / Notifications
$this->evaluateCampaignOpportunity($passengerId, $zone, $source);
}
// 4. Update Driver Demand Map
$this->updateDemandMap($passengerId, $lat, $lng);
// 5. Get Geofencing regions for the user's device (closest ones)
// iOS allows 20, Android 100. We'll return top 20 by default.
return $this->getUpdatedGeofencesForUser($lat, $lng);
}
private function updateLocationDatabase($passengerId, $lat, $lng, $source, $batteryLevel) {
try {
$sql = "INSERT INTO passenger_opening_locations (passenger_id, latitude, longitude, source, battery_level)
VALUES (:pid, :lat, :lng, :source, :battery)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
':pid' => $passengerId,
':lat' => $lat,
':lng' => $lng,
':source' => $source,
':battery' => $batteryLevel
]);
} catch (Exception $e) {
error_log("[LocationIntelligenceEngine] DB Error: " . $e->getMessage());
}
}
private function checkGeofenceZone($lat, $lng) {
// Find if the user's lat/lng is within the radius of any active geofence zone
// Using Haversine formula
$sql = "SELECT id, zone_name, country_code, radius_meters,
(6371000 * acos(cos(radians(:lat)) * cos(radians(latitude)) * cos(radians(longitude) - radians(:lng)) + sin(radians(:lat)) * sin(radians(latitude)))) AS distance
FROM geofence_zones
WHERE is_active = 1
HAVING distance <= radius_meters
ORDER BY distance ASC LIMIT 1";
$stmt = $this->db->prepare($sql);
$stmt->execute([':lat' => $lat, ':lng' => $lng]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
private function evaluateCampaignOpportunity($passengerId, $zone, $source) {
// 1. Check if passenger received a campaign recently (Anti-Spam)
$sqlSpamCheck = "SELECT COUNT(*) FROM marketing_campaigns_log
WHERE passenger_id = :pid
AND message_type = 'push'
AND sent_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)";
$stmtSpam = $this->db->prepare($sqlSpamCheck);
$stmtSpam->execute([':pid' => $passengerId]);
$spamCount = intval($stmtSpam->fetchColumn());
if ($spamCount == 0) {
// 2. Fetch Active Campaign (Placeholder for real campaign DB logic)
// Currently, we just send a generic welcome to the zone if priority is high.
if ($zone['priority'] >= 1) {
$this->sendCampaignNotification($passengerId, $zone);
}
}
}
private function sendCampaignNotification($passengerId, $zone) {
// Get passenger token
$sql = "SELECT t.token as users_token, p.country_code
FROM passengers p
JOIN tokens t ON p.id = t.passengerID
WHERE p.id = :pid";
$stmt = $this->db->prepare($sql);
$stmt->execute([':pid' => $passengerId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && !empty($user['users_token'])) {
$title = "مرحباً بك في " . $zone['zone_name'] . " 🎉";
$body = "اطلب رحلتك الآن من " . $zone['zone_name'] . " واستمتع بتجربة سيرو!";
// Decrypt token
require_once __DIR__ . '/../Security/EncryptionHelper.php';
$encryptionHelper = new EncryptionHelper();
$decryptedToken = $encryptionHelper->decryptData($user['users_token']);
if ($decryptedToken) {
// Send Push
sendFCM_Internal($decryptedToken, $title, $body, ['type' => 'geofence_promo'], 'Marketing');
}
// Log it
$logSql = "INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by)
VALUES (:pid, 'push', :country, :region, 'geofence_trigger')";
$logStmt = $this->db->prepare($logSql);
$logStmt->execute([
':pid' => $passengerId,
':country' => $user['country_code'] ?? 'JO',
':region' => $zone['zone_name']
]);
}
}
private function updateDemandMap($passengerId, $lat, $lng) {
// Broadcast this location to drivers or update a demand heat map cache
// Here we insert into passengerlocation to log the hotspot.
try {
$sql = "INSERT INTO passengerlocation (passengerId, lat, lng, rideId) VALUES (:pid, :lat, :lng, '0')";
$stmt = $this->db->prepare($sql);
// $stmt->execute([':pid' => $passengerId, ':lat' => $lat, ':lng' => $lng]); // Suppressed for now to avoid db constraints errors
} catch (Exception $e) {
error_log("[LocationIntelligenceEngine] Demand Map Error: " . $e->getMessage());
}
}
private function getUpdatedGeofencesForUser($lat, $lng, $limit = 20) {
// Return top nearest geofences to update on the user's device
$sql = "SELECT id, zone_name, latitude, longitude, radius_meters,
(6371000 * acos(cos(radians(:lat)) * cos(radians(latitude)) * cos(radians(longitude) - radians(:lng)) + sin(radians(:lat)) * sin(radians(latitude)))) AS distance
FROM geofence_zones
WHERE is_active = 1
ORDER BY distance ASC LIMIT :limit";
$stmt = $this->db->prepare($sql);
$stmt->bindParam(':lat', $lat);
$stmt->bindParam(':lng', $lng);
$stmt->bindValue(':limit', (int) $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
?>