Update: 2026-06-30 22:43:38
This commit is contained in:
42
backend/api/location/sync_location.php
Normal file
42
backend/api/location/sync_location.php
Normal 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.']);
|
||||
}
|
||||
?>
|
||||
96
backend/bot/cron_silent_push_inactive.php
Normal file
96
backend/bot/cron_silent_push_inactive.php
Normal 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;
|
||||
}
|
||||
?>
|
||||
134
backend/core/Services/LocationIntelligenceEngine.php
Normal file
134
backend/core/Services/LocationIntelligenceEngine.php
Normal 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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user