Update: 2026-06-30 22:43:38

This commit is contained in:
Hamza-Ayed
2026-06-30 22:43:38 +03:00
parent 1b5d6eae44
commit 26ae0124c8
4 changed files with 351 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
<?php
/**
* sync_location.php
* Unified endpoint for receiving passenger location updates from:
* - App Usage (Primary)
* - Geofencing Events (Secondary)
* - Silent Push Wakeups (Tertiary)
*/
require_once __DIR__ . '/../../connect.php';
// require_once __DIR__ . '/../../functions.php';
require_once __DIR__ . '/../../core/Services/LocationIntelligenceEngine.php';
// Validate JWT or traditional auth if needed. For now, rely on standard filterRequest if used.
$passengerId = filterRequest('passenger_id');
$lat = filterRequest('lat', 'float');
$lng = filterRequest('lng', 'float');
$source = filterRequest('source') ?? 'app_usage'; // 'app_usage', 'geofence', 'silent_push'
$batteryLevel = filterRequest('battery_level', 'int');
if (!$passengerId || !$lat || !$lng) {
http_response_code(400);
echo json_encode(['status' => 'failure', 'message' => 'Missing required parameters (passenger_id, lat, lng).']);
exit;
}
try {
$engine = new LocationIntelligenceEngine($con);
$newGeofences = $engine->processLocationUpdate($passengerId, $lat, $lng, $source, $batteryLevel);
// Respond with success and optionally new geofences
echo json_encode([
'status' => 'success',
'message' => 'Location synced successfully',
'update_geofences' => $newGeofences // App can use this array to update device geofencing regions
]);
} catch (Exception $e) {
error_log("[sync_location.php] Error: " . $e->getMessage());
http_response_code(500);
echo json_encode(['status' => 'failure', 'message' => 'Internal server error.']);
}
?>

View File

@@ -0,0 +1,96 @@
<?php
/**
* cron_silent_push_inactive.php
* Cron job to send a Silent Push Notification to inactive users,
* forcing their app to wake up in the background and report their current location.
* Run this periodically (e.g., every few hours or once a day).
*/
require_once __DIR__ . '/../connect.php';
require_once __DIR__ . '/../core/Services/FcmService.php';
// Target users who haven't updated their location in the last 24 hours
// but have a valid FCM token.
// Adjust the timeframe based on your strategy.
$sql = "
SELECT p.id as passenger_id, t.token
FROM passengers p
JOIN tokens t ON p.id = t.passengerID
LEFT JOIN passenger_opening_locations pol ON p.id = pol.start_location
WHERE (pol.date IS NULL OR pol.date < DATE_SUB(NOW(), INTERVAL 24 HOUR))
GROUP BY p.id, t.token
LIMIT 500 -- Batch size to avoid overloading the server or getting rate-limited
";
$stmt = $con->prepare($sql);
$stmt->execute();
$inactiveUsers = $stmt->fetchAll(PDO::FETCH_ASSOC);
$sentCount = 0;
$fcmService = new FcmService(); // Assuming we have an FcmService or we can use sendFcmNotification directly if it's in functions.php
// In Siro, encryptionHelper is typically used for tokens. Assuming it's required here:
require_once __DIR__ . '/../core/Security/EncryptionHelper.php';
$encryptionHelper = new EncryptionHelper();
foreach ($inactiveUsers as $user) {
$passengerId = $user['passenger_id'];
$encryptedToken = $user['token'];
$decryptedToken = $encryptionHelper->decryptData($encryptedToken);
if ($decryptedToken) {
// Send a silent push notification.
// A silent push doesn't have a 'notification' payload (title/body),
// it only has a 'data' payload with 'content-available' => 1 for iOS.
$dataPayload = [
'type' => 'location_sync_request',
'action' => 'wake_up'
];
// This is a pseudo implementation relying on your existing push notification setup.
// Ensure that your FCM implementation correctly maps 'content-available' for iOS.
sendSilentFcmNotification($decryptedToken, $dataPayload);
$sentCount++;
}
}
echo "Silent Push triggered for $sentCount inactive users.\n";
/**
* Helper to send Silent FCM
*/
function sendSilentFcmNotification($token, $data) {
// Basic curl request to FCM API for silent push
$url = 'https://fcm.googleapis.com/fcm/send';
// Replace with your actual server key
$serverKey = getenv('FCM_SERVER_KEY') ?: 'YOUR_LEGACY_SERVER_KEY';
$fields = [
'to' => $token,
'data' => $data,
'content_available' => true, // Required for iOS to wake up in background
'priority' => 'high' // Sometimes required to wake up Android
];
$headers = [
'Authorization: key=' . $serverKey,
'Content-Type: application/json'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
?>

View File

@@ -0,0 +1,134 @@
<?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) {
// 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) {
// Avoid spamming if source is silent_push, maybe only do it for geofence or app_usage
if ($source === 'silent_push') {
return; // Don't trigger active campaigns on silent push to save battery and avoid weird timing
}
// 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. Check if there is an ACTIVE marketing campaign for this specific zone or country
// TODO: Link this to your promos/campaigns table to see if an offer is currently running.
// DO NOT send a generic "Welcome" notification as it is annoying to users.
// We only send a push if there's a real incentive (e.g. discount code).
// Example of what will be here:
// $campaign = $this->getActiveCampaignForZone($zone['id']);
// if ($campaign) {
// $this->sendCampaignNotification($passengerId, $campaign);
// }
}
}
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);
}
}
?>

View File

@@ -1972,3 +1972,82 @@ CREATE TABLE IF NOT EXISTS `scraped_competitor_prices` (
KEY `idx_start_location` (`start_location`),
KEY `idx_country_code` (`country_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Location Intelligence Engine Schema Migrations
-- 1. Create table for Geofence Zones
CREATE TABLE IF NOT EXISTS `geofence_zones` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`zone_name` VARCHAR(255) NOT NULL,
`latitude` DECIMAL(10, 8) NOT NULL,
`longitude` DECIMAL(11, 8) NOT NULL,
`radius_meters` INT NOT NULL DEFAULT 2000,
`priority` INT DEFAULT 1,
`is_active` BOOLEAN DEFAULT TRUE,
`country_code` VARCHAR(2) DEFAULT 'JO',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 2. Alter passenger_opening_locations to support source tracking
-- Note: 'source' allows tracking if the location update came from app usage, a geofence trigger, or a silent push
ALTER TABLE `passenger_opening_locations`
ADD COLUMN `source` ENUM('app_usage', 'geofence', 'silent_push') DEFAULT 'app_usage',
ADD COLUMN `battery_level` INT DEFAULT NULL;
-- 3. Insert specific zones for testing and real usage (Jordan, Syria, Egypt)
INSERT INTO `geofence_zones` (`zone_name`, `latitude`, `longitude`, `radius_meters`, `priority`, `country_code`)
VALUES
-- Jordan (Amman) - 15 Zones
('Queen Alia International Airport', 31.7225, 35.9933, 3000, 10, 'JO'),
('Abdali Boulevard', 31.9631, 35.9122, 1000, 5, 'JO'),
('University of Jordan', 32.0129, 35.8741, 1500, 8, 'JO'),
('Taj Mall', 31.9427, 35.8895, 1000, 7, 'JO'),
('City Mall', 31.9749, 35.8362, 1000, 7, 'JO'),
('Mecca Mall', 31.9765, 35.8427, 1000, 7, 'JO'),
('Al Hussein Public Parks', 31.9868, 35.8286, 2000, 6, 'JO'),
('Amman Citadel', 31.9544, 35.9355, 1500, 6, 'JO'),
('Roman Theater', 31.9516, 35.9394, 1000, 6, 'JO'),
('Rainbow Street', 31.9497, 35.9231, 1000, 8, 'JO'),
('Jordan Hospital', 31.9612, 35.9039, 1000, 5, 'JO'),
('King Hussein Business Park', 31.9723, 35.8277, 1500, 8, 'JO'),
('Royal Automobile Museum', 31.9833, 35.8242, 1000, 4, 'JO'),
('Galleria Mall', 31.9542, 35.8564, 1000, 7, 'JO'),
('Sweifieh Village', 31.9515, 35.8519, 1000, 8, 'JO'),
-- Syria (Damascus) - 15 Zones
('Damascus International Airport', 33.4114, 36.5147, 3000, 10, 'SY'),
('Umayyad Mosque', 33.5116, 36.3067, 1000, 9, 'SY'),
('Al-Hamidiyah Souq', 33.5106, 36.3023, 1000, 8, 'SY'),
('Cham City Center', 33.5186, 36.2737, 1000, 7, 'SY'),
('Damascus University', 33.5103, 36.2894, 1500, 8, 'SY'),
('Mount Qasioun', 33.5350, 36.2750, 2000, 6, 'SY'),
('Al-Jalaa Sports Hall', 33.5133, 36.2625, 1500, 5, 'SY'),
('Dama Rose Hotel', 33.5150, 36.2870, 1000, 7, 'SY'),
('National Museum of Damascus', 33.5115, 36.2905, 1000, 6, 'SY'),
('Bab Tuma', 33.5126, 36.3150, 1000, 8, 'SY'),
('Malki Park', 33.5230, 36.2830, 1000, 6, 'SY'),
('Al-Assad University Hospital', 33.5042, 36.2575, 1500, 7, 'SY'),
('Al-Jalaa Street', 33.5145, 36.2645, 1000, 7, 'SY'),
('Mezzeh 86', 33.5020, 36.2420, 2000, 5, 'SY'),
('Baramkeh Bus Station', 33.5065, 36.2890, 1500, 8, 'SY'),
-- Egypt (Cairo) - 20 Zones
('Cairo International Airport', 30.1219, 31.4056, 3000, 10, 'EG'),
('The Egyptian Museum', 30.0478, 31.2336, 1000, 9, 'EG'),
('Cairo Tower', 30.0459, 31.2243, 1000, 8, 'EG'),
('Al-Azhar Park', 30.0405, 31.2643, 1500, 7, 'EG'),
('Khan el-Khalili', 30.0477, 31.2621, 1000, 8, 'EG'),
('Cairo University', 30.0276, 31.2101, 2000, 8, 'EG'),
('Mall of Arabia', 30.0075, 30.9734, 2000, 8, 'EG'),
('City Stars Mall', 30.0734, 31.3304, 2000, 9, 'EG'),
('Cairo Festival City Mall', 30.0305, 31.4061, 2000, 9, 'EG'),
('Giza Pyramids Entrance', 29.9792, 31.1342, 3000, 10, 'EG'),
('Smart Village', 30.0760, 30.0150, 2500, 7, 'EG'),
('Al-Ahly Club Gezira', 30.0440, 31.2230, 1000, 7, 'EG'),
('Zamalek', 30.0626, 31.2166, 2000, 8, 'EG'),
('Tahrir Square', 30.0444, 31.2357, 1500, 9, 'EG'),
('Heliopolis Korba', 30.0880, 31.3260, 1500, 7, 'EG'),
('Nasr City Abbas El Akkad', 30.0592, 31.3392, 2000, 8, 'EG'),
('Maadi Street 9', 29.9602, 31.2585, 1500, 8, 'EG'),
('Mall of Egypt', 29.9705, 30.9856, 2000, 9, 'EG'),
('Cairo Opera House', 30.0427, 31.2238, 1000, 7, 'EG'),
('Al-Azhar Mosque', 30.0457, 31.2627, 1000, 6, 'EG');