Update: 2026-06-21 18:58:05
This commit is contained in:
@@ -60,6 +60,8 @@ ALLOWED_BACKEND_IDS=siromove-backend-01,siromove-backend-02
|
||||
ALLOWED_SOCKET_URLS=https://location.siromove.com,https://socket.siromove.com
|
||||
SOCKET_API_TIMEOUT=10
|
||||
SOCKET_INTERNAL_KEY=<CHANGE_ME_INTERNAL_KEY>
|
||||
LOCATION_SERVER_URL=http://location.intaleq.xyz:2021
|
||||
RIDE_SOCKET_URL=http://location.intaleq.xyz:3031
|
||||
|
||||
# =============================================================================
|
||||
# CORS Configuration
|
||||
@@ -75,6 +77,12 @@ LOG_LEVEL=info
|
||||
LOG_PATH=/var/log/siro-api/
|
||||
SECURITY_LOG_PATH=/var/log/siro-api/security/
|
||||
|
||||
# =============================================================================
|
||||
# Bot Configuration
|
||||
# =============================================================================
|
||||
BOT_SECRET_KEY=<CHANGE_ME_BOT_SECRET>
|
||||
FEMALE_GENDER_HASH=<CHANGE_ME_FEMALE_HASH>
|
||||
|
||||
# =============================================================================
|
||||
# Firebase Configuration
|
||||
# =============================================================================
|
||||
@@ -116,6 +124,11 @@ SECRET_KEY_HMAC=<CHANGE_ME_HMAC_SECRET_FOR_SIGNED_URLS>
|
||||
# =============================================================================
|
||||
FP_PEPPER=<CHANGE_ME_FINGERPRINT_PEPPER>
|
||||
|
||||
# =============================================================================
|
||||
# Gemini AI Configuration
|
||||
# =============================================================================
|
||||
GEMINI_API_KEY=<CHANGE_ME_GEMINI_KEY>
|
||||
|
||||
# =============================================================================
|
||||
# Feature Flags
|
||||
# =============================================================================
|
||||
|
||||
64
backend/Admin/marketing/get_campaigns_log.php
Normal file
64
backend/Admin/marketing/get_campaigns_log.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// Admin/marketing/get_campaigns_log.php
|
||||
// API Endpoint to fetch marketing campaign delivery logs for Admin dashboard
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// 1. Authorize Admin/Super Admin
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$limit = filterRequest('limit', 'int') ?? 50;
|
||||
$countryCode = filterRequest('country_code');
|
||||
|
||||
$sql = "SELECT l.*, p.first_name, p.last_name
|
||||
FROM marketing_campaigns_log l
|
||||
LEFT JOIN passengers p ON p.id = l.passenger_id";
|
||||
|
||||
$params = [];
|
||||
if ($countryCode) {
|
||||
$sql .= " WHERE l.country_code = :country";
|
||||
$params[':country'] = strtoupper($countryCode);
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY l.sent_at DESC LIMIT :limit";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
foreach ($params as $key => $val) {
|
||||
$stmt->bindValue($key, $val);
|
||||
}
|
||||
$stmt->execute();
|
||||
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Decrypt names or just return them
|
||||
// (Names are not encrypted in this schema, only phones are, so we can return directly)
|
||||
|
||||
// Aggregate statistics for Dashboard charts
|
||||
$sqlStats = "SELECT message_type, COUNT(*) as count
|
||||
FROM marketing_campaigns_log";
|
||||
if ($countryCode) {
|
||||
$sqlStats .= " WHERE country_code = :country";
|
||||
$stmtStats = $con->prepare($sqlStats);
|
||||
$stmtStats->execute([':country' => strtoupper($countryCode)]);
|
||||
} else {
|
||||
$stmtStats = $con->prepare($sqlStats);
|
||||
$stmtStats->execute();
|
||||
}
|
||||
$stats = $stmtStats->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
jsonSuccess([
|
||||
'logs' => $logs,
|
||||
'stats' => $stats
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[get_campaigns_log.php] Error: " . $e->getMessage());
|
||||
jsonError("Failed to fetch campaigns log: " . $e->getMessage());
|
||||
}
|
||||
62
backend/Admin/marketing/get_market_anomalies.php
Normal file
62
backend/Admin/marketing/get_market_anomalies.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// Admin/marketing/get_market_anomalies.php
|
||||
// API Endpoint for Admin App (Flutter) to fetch price anomalies
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// 1. Authorize role
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Fetch anomalies
|
||||
try {
|
||||
$limit = filterRequest('limit', 'int') ?? 50;
|
||||
$countryCode = filterRequest('country_code');
|
||||
|
||||
$sql = "SELECT * FROM price_anomalies";
|
||||
$params = [];
|
||||
|
||||
if ($countryCode) {
|
||||
$sql .= " WHERE country_code = :country";
|
||||
$params[':country'] = strtoupper($countryCode);
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY created_at DESC LIMIT :limit";
|
||||
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||
foreach ($params as $key => $val) {
|
||||
$stmt->bindValue($key, $val);
|
||||
}
|
||||
$stmt->execute();
|
||||
$anomalies = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Fetch some recent competitor prices for context
|
||||
$sqlPrices = "SELECT * FROM competitor_prices";
|
||||
$paramsPrices = [];
|
||||
if ($countryCode) {
|
||||
$sqlPrices .= " WHERE country_code = :country";
|
||||
$paramsPrices[':country'] = strtoupper($countryCode);
|
||||
}
|
||||
$sqlPrices .= " ORDER BY created_at DESC LIMIT 20";
|
||||
$stmtPrices = $con->prepare($sqlPrices);
|
||||
foreach ($paramsPrices as $key => $val) {
|
||||
$stmtPrices->bindValue($key, $val);
|
||||
}
|
||||
$stmtPrices->execute();
|
||||
$recentPrices = $stmtPrices->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
jsonSuccess([
|
||||
'anomalies' => $anomalies,
|
||||
'recent_prices' => $recentPrices
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[get_market_anomalies.php] Error: " . $e->getMessage());
|
||||
jsonError("Failed to fetch market anomalies: " . $e->getMessage());
|
||||
}
|
||||
77
backend/Admin/marketing/get_price_comparison.php
Normal file
77
backend/Admin/marketing/get_price_comparison.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$countryCode = filterRequest('country_code');
|
||||
|
||||
// 1. Hourly competitor price averages (last 24h)
|
||||
$compSql = "SELECT
|
||||
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') AS hour_bucket,
|
||||
AVG(price_per_km) AS avg_price_per_km,
|
||||
COUNT(*) AS sample_count
|
||||
FROM competitor_prices
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)";
|
||||
$compParams = [];
|
||||
if ($countryCode) {
|
||||
$compSql .= " AND country_code = :country";
|
||||
$compParams[':country'] = strtoupper($countryCode);
|
||||
}
|
||||
$compSql .= " GROUP BY hour_bucket ORDER BY hour_bucket ASC LIMIT 24";
|
||||
|
||||
$stmt = $con->prepare($compSql);
|
||||
foreach ($compParams as $k => $v) {
|
||||
$stmt->bindValue($k, $v);
|
||||
}
|
||||
$stmt->execute();
|
||||
$hourlyData = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 2. PCI by region — group competitor prices by ~0.02° grid cells
|
||||
$pciSql = "SELECT
|
||||
ROUND(from_latitude * 50, 0) / 50 AS lat_group,
|
||||
ROUND(from_longitude * 50, 0) / 50 AS lng_group,
|
||||
competitor_name,
|
||||
AVG(price_per_km) AS avg_price_per_km,
|
||||
COUNT(*) AS samples
|
||||
FROM competitor_prices
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)";
|
||||
$pciParams = [];
|
||||
if ($countryCode) {
|
||||
$pciSql .= " AND country_code = :country2";
|
||||
$pciParams[':country2'] = strtoupper($countryCode);
|
||||
}
|
||||
$pciSql .= " GROUP BY lat_group, lng_group, competitor_name
|
||||
ORDER BY samples DESC LIMIT 20";
|
||||
|
||||
$stmtPci = $con->prepare($pciSql);
|
||||
foreach ($pciParams as $k => $v) {
|
||||
$stmtPci->bindValue($k, $v);
|
||||
}
|
||||
$stmtPci->execute();
|
||||
$pciData = $stmtPci->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 3. Siro base prices by category (from kazan table)
|
||||
$siroSql = "SELECT speedPrice, comfortPrice, awfarPrice, ladyPrice, electricPrice, vanPrice
|
||||
FROM kazan WHERE country = :country3 LIMIT 1";
|
||||
$countryNameMap = ['SY' => 'Syria', 'JO' => 'Jordan', 'EG' => 'Egypt', 'IQ' => 'Iraq'];
|
||||
$siroCountry = $countryNameMap[strtoupper($countryCode ?: 'SY')] ?? 'Syria';
|
||||
|
||||
$stmtSiro = $con->prepare($siroSql);
|
||||
$stmtSiro->execute([':country3' => $siroCountry]);
|
||||
$siroPrices = $stmtSiro->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
jsonSuccess([
|
||||
'hourly_competitor_prices' => $hourlyData,
|
||||
'pci_regions' => $pciData,
|
||||
'siro_base_prices' => $siroPrices ?: [],
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[get_price_comparison.php] Error: " . $e->getMessage());
|
||||
jsonError("Failed to fetch price comparison: " . $e->getMessage());
|
||||
}
|
||||
55
backend/Admin/marketing/get_telemetry.php
Normal file
55
backend/Admin/marketing/get_telemetry.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$countryCode = filterRequest('country_code');
|
||||
|
||||
$countSql = "SELECT COUNT(*) FROM marketing_campaigns_log";
|
||||
$params = [];
|
||||
if ($countryCode) {
|
||||
$countSql .= " WHERE country_code = :country";
|
||||
$params[':country'] = strtoupper($countryCode);
|
||||
}
|
||||
$stmt = $con->prepare($countSql);
|
||||
foreach ($params as $key => $val) {
|
||||
$stmt->bindValue($key, $val);
|
||||
}
|
||||
$stmt->execute();
|
||||
$campaignCount = (int)$stmt->fetchColumn();
|
||||
|
||||
$estTokensPerCampaign = 3250;
|
||||
$estCostPerCampaign = 0.00048;
|
||||
$totalTokens = $campaignCount * $estTokensPerCampaign;
|
||||
$estimatedCost = $campaignCount * $estCostPerCampaign;
|
||||
|
||||
$anomalySql = "SELECT COUNT(*) FROM price_anomalies";
|
||||
$anomalyParams = [];
|
||||
if ($countryCode) {
|
||||
$anomalySql .= " WHERE country_code = :country2";
|
||||
$anomalyParams[':country2'] = strtoupper($countryCode);
|
||||
}
|
||||
$stmtAnomaly = $con->prepare($anomalySql);
|
||||
foreach ($anomalyParams as $key => $val) {
|
||||
$stmtAnomaly->bindValue($key, $val);
|
||||
}
|
||||
$stmtAnomaly->execute();
|
||||
$anomalyCount = (int)$stmtAnomaly->fetchColumn();
|
||||
|
||||
jsonSuccess([
|
||||
'api_requests_count' => $campaignCount,
|
||||
'total_tokens_used' => $totalTokens,
|
||||
'estimated_cost_usd' => round($estimatedCost, 6),
|
||||
'campaigns_count' => $campaignCount,
|
||||
'anomalies_count' => $anomalyCount,
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[get_telemetry.php] Error: " . $e->getMessage());
|
||||
jsonError("Failed to fetch telemetry: " . $e->getMessage());
|
||||
}
|
||||
200
backend/Admin/marketing/trigger_campaign.php
Normal file
200
backend/Admin/marketing/trigger_campaign.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// Admin/marketing/trigger_campaign.php
|
||||
// API Endpoint to trigger Gemini AI campaign generation and dispatch
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
require_once __DIR__ . '/../../core/Services/SiroGeminiService.php';
|
||||
|
||||
// 1. Authorize Admin/Super Admin
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Filter inputs
|
||||
$regionName = filterRequest('region_name') ?? 'Damascus';
|
||||
$countryCode = filterRequest('country_code') ?? 'SY';
|
||||
$siroBasePrice = filterRequest('siro_base_price', 'float') ?? 10000.0;
|
||||
|
||||
try {
|
||||
// 3. Fetch recent competitor prices for this region to supply context to Gemini
|
||||
$sqlPrices = "SELECT competitor_name, total_price, distance_km
|
||||
FROM competitor_prices
|
||||
WHERE country_code = :country
|
||||
ORDER BY created_at DESC LIMIT 10";
|
||||
$stmtPrices = $con->prepare($sqlPrices);
|
||||
$stmtPrices->execute([':country' => strtoupper($countryCode)]);
|
||||
$competitorPrices = $stmtPrices->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($competitorPrices)) {
|
||||
// Fallback mock context if no competitor pricing has been scraped yet
|
||||
$competitorPrices = [
|
||||
['competitor_name' => 'yallago', 'total_price' => 12000, 'distance_km' => 5],
|
||||
['competitor_name' => 'zaken', 'total_price' => 11500, 'distance_km' => 5]
|
||||
];
|
||||
}
|
||||
|
||||
// 4. Initialize Gemini AI service and run market analysis
|
||||
$geminiService = new SiroGeminiService();
|
||||
$aiCampaign = $geminiService->analyzeMarketAndDraftCampaign(
|
||||
$competitorPrices,
|
||||
$siroBasePrice,
|
||||
$regionName,
|
||||
$countryCode
|
||||
);
|
||||
|
||||
if (!$aiCampaign) {
|
||||
jsonError("Failed to generate campaign via Gemini AI service.");
|
||||
}
|
||||
|
||||
// Check if campaign is recommended
|
||||
$opportunityDetected = $aiCampaign['opportunity_detected'] ?? false;
|
||||
if (!$opportunityDetected) {
|
||||
jsonSuccess([
|
||||
'campaign_created' => false,
|
||||
'reason' => 'Gemini AI determined no marketing opportunity is present based on current pricing structures.',
|
||||
'ai_analysis' => $aiCampaign
|
||||
]);
|
||||
}
|
||||
|
||||
$promoCode = $aiCampaign['promo_code'] ?? 'SIROGO10';
|
||||
$discountVal = $aiCampaign['discount_percentage'] ?? 10;
|
||||
$pushTitle = $aiCampaign['push_title'] ?? 'خصومات مميزة من سيرو!';
|
||||
$pushBody = $aiCampaign['push_body'] ?? 'وفر أكثر على رحلتك القادمة معنا.';
|
||||
$smsBody = $aiCampaign['sms_body'] ?? 'اشتقنا لك! عد إلينا ووفر أكثر مع الرمز الترويجي الخاص بك.';
|
||||
|
||||
// 5. Target Passengers in the specified country
|
||||
// Check passenger_opening_locations for target audiences
|
||||
$sqlTarget = "SELECT DISTINCT l.passenger_id
|
||||
FROM passenger_opening_locations l
|
||||
WHERE l.country_code = :country";
|
||||
$stmtTarget = $con->prepare($sqlTarget);
|
||||
$stmtTarget->execute([':country' => strtoupper($countryCode)]);
|
||||
$targets = $stmtTarget->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$sentFcm = 0;
|
||||
$sentSms = 0;
|
||||
$sentWhatsApp = 0;
|
||||
$dispatchedPassengers = [];
|
||||
|
||||
// 6. Save broadcast promo for this campaign (Option 1 - promos table adjustment)
|
||||
$sqlPromo = "INSERT INTO promos
|
||||
(promo_code, amount, description, passengerID, source, validity_start_date, validity_end_date)
|
||||
VALUES (:code, :amount, :desc, 'all', 'ai_generated', CURDATE(), DATE_ADD(CURDATE(), INTERVAL 7 DAY))";
|
||||
$stmtPromo = $con->prepare($sqlPromo);
|
||||
$stmtPromo->execute([
|
||||
':code' => $promoCode,
|
||||
':amount' => (string)$discountVal,
|
||||
':desc' => "AI Dynamic Promo: $promoCode ($discountVal%)"
|
||||
]);
|
||||
|
||||
foreach ($targets as $target) {
|
||||
$passengerId = $target['passenger_id'];
|
||||
|
||||
// Enforce anti-spam: check if passenger received any SMS/WhatsApp campaign in the last 24 hours
|
||||
$sqlSpamCheck = "SELECT COUNT(*) FROM marketing_campaigns_log
|
||||
WHERE passenger_id = :pid
|
||||
AND message_type IN ('sms', 'whatsapp')
|
||||
AND sent_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)";
|
||||
$stmtSpam = $con->prepare($sqlSpamCheck);
|
||||
$stmtSpam->execute([':pid' => $passengerId]);
|
||||
$spamCount = intval($stmtSpam->fetchColumn());
|
||||
|
||||
// Check if passenger has active FCM token
|
||||
$sqlToken = "SELECT token FROM tokens WHERE passengerID = :pid LIMIT 1";
|
||||
$stmtToken = $con->prepare($sqlToken);
|
||||
$stmtToken->execute([':pid' => $passengerId]);
|
||||
$fcmToken = $stmtToken->fetchColumn();
|
||||
|
||||
if ($fcmToken) {
|
||||
// Send FCM Push Notification (Free channel - no anti-spam restriction needed)
|
||||
$fcmData = [
|
||||
'type' => 'marketing_campaign',
|
||||
'promo_code' => $promoCode,
|
||||
'discount' => (string)$discountVal
|
||||
];
|
||||
|
||||
$fcmResult = sendFcmNotification(
|
||||
$fcmToken,
|
||||
$pushTitle,
|
||||
$pushBody,
|
||||
$fcmData,
|
||||
'Marketing',
|
||||
'notification'
|
||||
);
|
||||
|
||||
if ($fcmResult['status'] === 'success') {
|
||||
$sentFcm++;
|
||||
// Log campaign dispatch
|
||||
$logStmt = $con->prepare("INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by) VALUES (?, 'push', ?, ?, 'autopilot')");
|
||||
$logStmt->execute([$passengerId, $countryCode, $regionName]);
|
||||
$dispatchedPassengers[] = $passengerId;
|
||||
}
|
||||
} else {
|
||||
// Churned user (Deleted the app / No token) -> Send WhatsApp or SMS
|
||||
// Check anti-spam first to prevent unnecessary marketing cost
|
||||
if ($spamCount === 0) {
|
||||
// Fetch and decrypt passenger phone number
|
||||
$sqlUser = "SELECT phone FROM passengers WHERE id = :pid LIMIT 1";
|
||||
$stmtUser = $con->prepare($sqlUser);
|
||||
$stmtUser->execute([':pid' => $passengerId]);
|
||||
$encPhone = $stmtUser->fetchColumn();
|
||||
|
||||
if ($encPhone) {
|
||||
$decryptedPhone = $encryptionHelper->decryptData($encPhone);
|
||||
if ($decryptedPhone) {
|
||||
// Send WhatsApp (or fallback to SMS simulation)
|
||||
$waResult = sendWhatsAppFromServer($decryptedPhone, $smsBody);
|
||||
|
||||
if ($waResult && ($waResult['status'] ?? '') === 'success') {
|
||||
$sentWhatsApp++;
|
||||
$logStmt = $con->prepare("INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by) VALUES (?, 'whatsapp', ?, ?, 'autopilot')");
|
||||
$logStmt->execute([$passengerId, $countryCode, $regionName]);
|
||||
$dispatchedPassengers[] = $passengerId;
|
||||
} else {
|
||||
// Fallback to SMS simulation
|
||||
$sentSms++;
|
||||
$logStmt = $con->prepare("INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by) VALUES (?, 'sms', ?, ?, 'autopilot')");
|
||||
$logStmt->execute([$passengerId, $countryCode, $regionName]);
|
||||
$dispatchedPassengers[] = $passengerId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log the audit event for Admin action
|
||||
logAudit(
|
||||
$con,
|
||||
$user_id ?? 'admin_system',
|
||||
'trigger_marketing_campaign',
|
||||
'promos',
|
||||
$promoCode,
|
||||
['promo_code' => $promoCode, 'targets_count' => count($dispatchedPassengers)]
|
||||
);
|
||||
|
||||
jsonSuccess([
|
||||
'campaign_created' => true,
|
||||
'promo_code' => $promoCode,
|
||||
'discount_percentage' => $discountVal,
|
||||
'push_notification' => [
|
||||
'title' => $pushTitle,
|
||||
'body' => $pushBody,
|
||||
'sent_count' => $sentFcm
|
||||
],
|
||||
'whatsapp_sms' => [
|
||||
'body' => $smsBody,
|
||||
'whatsapp_sent_count' => $sentWhatsApp,
|
||||
'sms_sent_count' => $sentSms
|
||||
],
|
||||
'total_dispatched' => count($dispatchedPassengers)
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[trigger_campaign.php] Error: " . $e->getMessage());
|
||||
jsonError("Failed to trigger marketing campaign: " . $e->getMessage());
|
||||
}
|
||||
@@ -18,17 +18,19 @@ try {
|
||||
// 1. Ensure Table Exists
|
||||
$sql = "
|
||||
CREATE TABLE IF NOT EXISTS competitor_prices (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
app_name VARCHAR(50) NOT NULL,
|
||||
start_lat DECIMAL(10,8) NOT NULL,
|
||||
start_lng DECIMAL(11,8) NOT NULL,
|
||||
end_lat DECIMAL(10,8) NOT NULL,
|
||||
end_lng DECIMAL(11,8) NOT NULL,
|
||||
distance_km FLOAT NOT NULL,
|
||||
price FLOAT NOT NULL,
|
||||
recorded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_app_time (app_name, recorded_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
competitor_name VARCHAR(50) NOT NULL,
|
||||
from_latitude VARCHAR(30) NOT NULL,
|
||||
from_longitude VARCHAR(30) NOT NULL,
|
||||
to_latitude VARCHAR(30) NOT NULL,
|
||||
to_longitude VARCHAR(30) NOT NULL,
|
||||
distance_km DECIMAL(8,2) NOT NULL,
|
||||
total_price DECIMAL(10,2) NOT NULL,
|
||||
price_per_km DECIMAL(8,2) NOT NULL,
|
||||
country_code VARCHAR(5) NOT NULL DEFAULT 'SY',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_competitor_country (competitor_name, country_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
";
|
||||
$con->exec($sql);
|
||||
|
||||
|
||||
@@ -17,7 +17,12 @@ try {
|
||||
// =========================================================
|
||||
|
||||
// 1. Security & Configuration
|
||||
$SECRET_KEY = getenv('BOT_SECRET_KEY') ?: 'SIRO_BOT_SUPER_SECRET_123';
|
||||
$SECRET_KEY = getenv('BOT_SECRET_KEY');
|
||||
if (!$SECRET_KEY) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Server configuration error: BOT_SECRET_KEY not set in environment']);
|
||||
exit;
|
||||
}
|
||||
$ALLOWED_DEVICES = ['SHAM_CASH_BOT_01', 'PRICE_SCRAPER_BOT_01'];
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
@@ -123,23 +128,25 @@ if ($method === 'GET') {
|
||||
$end_lat = (float)($result_data['end_lat'] ?? 0);
|
||||
$end_lng = (float)($result_data['end_lng'] ?? 0);
|
||||
|
||||
$pricePerKm = $distance_km > 0 ? ($price / $distance_km) : 0.0;
|
||||
$country_code = $result_data['country_code'] ?? 'SY';
|
||||
|
||||
// 1. Save to MySQL
|
||||
$stmt = $con->prepare("
|
||||
INSERT INTO competitor_prices
|
||||
(app_name, start_lat, start_lng, end_lat, end_lng, distance_km, price)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
(competitor_name, from_latitude, from_longitude, to_latitude, to_longitude, distance_km, total_price, price_per_km, country_code)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$stmt->execute([$app_name, $start_lat, $start_lng, $end_lat, $end_lng, $distance_km, $price]);
|
||||
$stmt->execute([$app_name, (string)$start_lat, (string)$start_lng, (string)$end_lat, (string)$end_lng, $distance_km, $price, $pricePerKm, $country_code]);
|
||||
|
||||
// 2. Save to Redis (Calculate Price Per KM)
|
||||
if ($distance_km > 0 && $price > 0) {
|
||||
$pricePerKm = $price / $distance_km;
|
||||
// Store in Redis (Main) to be used by Pricing Engine
|
||||
// Store recent 50 prices for the app
|
||||
$redis->lpush("competitor:price_history:$app_name", $pricePerKm);
|
||||
$redis->ltrim("competitor:price_history:$app_name", 0, 49);
|
||||
|
||||
error_log("[Bot Worker] Price Check $app_name: Dist $distance_km, Price $price");
|
||||
error_log("[Bot Worker] Price Check $app_name: Dist $distance_km, Price $price, Country $country_code");
|
||||
}
|
||||
} else {
|
||||
// It's a payment task
|
||||
|
||||
129
backend/core/Services/SiroGeminiService.php
Normal file
129
backend/core/Services/SiroGeminiService.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// core/Services/SiroGeminiService.php
|
||||
// Siro AI Market Analysis & Marketing Content Generation Service
|
||||
// ============================================================
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class SiroGeminiService {
|
||||
private ?string $apiKey;
|
||||
private string $baseUrl;
|
||||
|
||||
public function __construct() {
|
||||
$this->apiKey = getenv('GEMINI_API_KEY') ?: null;
|
||||
$this->baseUrl = "https://generativelanguage.googleapis.com/v1beta/models/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze market prices and generate target promotion response
|
||||
*
|
||||
* @param array $competitorPrices Array of competitor pricing information
|
||||
* @param float $siroBasePrice Current Siro base pricing for this region/country
|
||||
* @param string $regionName Name of the region/city
|
||||
* @param string $countryCode Country code (e.g. SY, JO, EG, IQ)
|
||||
* @param string $model Override AI model to use (default: gemini-1.5-flash)
|
||||
* @return array|null Decoded JSON response from Gemini or null on failure
|
||||
*/
|
||||
public function analyzeMarketAndDraftCampaign(
|
||||
array $competitorPrices,
|
||||
float $siroBasePrice,
|
||||
string $regionName,
|
||||
string $countryCode,
|
||||
string $model = 'gemini-1.5-flash'
|
||||
): ?array {
|
||||
if (!$this->apiKey) {
|
||||
error_log("[SiroGeminiService] API Key is missing.");
|
||||
return null;
|
||||
}
|
||||
|
||||
$dialect = match (strtoupper($countryCode)) {
|
||||
'SY' => 'السورية (الشامية)',
|
||||
'JO' => 'الأردنية',
|
||||
'EG' => 'المصرية',
|
||||
'IQ' => 'العراقية',
|
||||
default => 'العربية الفصحى البسيطة'
|
||||
};
|
||||
|
||||
$prompt = "
|
||||
أنت خبير تسويق ذكي ومحلل أسعار لتطبيق Siro لخدمات نقل الركاب.
|
||||
قم بتحليل أسعار المنافسين في منطقة '$regionName' وصياغة حملة تسويقية وعرض ترويجي منافس.
|
||||
|
||||
بيانات الإدخال:
|
||||
1. أسعار المنافسين الحالية: " . json_encode($competitorPrices, JSON_UNESCAPED_UNICODE) . "
|
||||
2. سعر رحلة Siro الأساسي الحالي: $siroBasePrice
|
||||
|
||||
المطلوب:
|
||||
1. دراسة الأسعار وتحديد هل توجد فرصة تسويقية واضحة لجذب الركاب (opportunity_detected: true/false).
|
||||
2. تحديد معامل التخفيض المقترح أو قيمة خصم مناسبة (discount_value) والنسبة المئوية (discount_percentage).
|
||||
3. كتابة رسالة تسويقية إعلانية جذابة وقصيرة جداً ومقنعة لإرسالها كإشعار (Push Notification) للركاب النشطين بالهجة $dialect.
|
||||
4. كتابة رسالة استعادة جذابة ومغرية وقصيرة لإرسالها عبر SMS أو WhatsApp للركاب المنقطعين بالهجة $dialect مع ذكر كود الخصم المقترح.
|
||||
5. اقتراح كود خصم مناسب للحملة (promo_code) ليكون سهل الحفظ ومناسباً للحدث.
|
||||
|
||||
الخرج المطلوب (يجب أن يكون JSON صالحاً تماماً وخالياً من أي شرح خارجي، باللغة العربية):
|
||||
{
|
||||
\"opportunity_detected\": true/false,
|
||||
\"recommended_price\": 12000,
|
||||
\"discount_percentage\": 15,
|
||||
\"promo_code\": \"SIROGO15\",
|
||||
\"push_title\": \"عنوان الإشعار\",
|
||||
\"push_body\": \"محتوى الإشعار القصير والمثير للاهتمام\",
|
||||
\"sms_body\": \"محتوى رسالة الاستعادة القصير والمقنع للـ SMS أو الواتساب\"
|
||||
}
|
||||
";
|
||||
|
||||
$apiUrl = $this->baseUrl . $model . ":generateContent?key=" . $this->apiKey;
|
||||
|
||||
$payload = [
|
||||
'contents' => [
|
||||
[
|
||||
'parts' => [
|
||||
['text' => $prompt]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$ch = curl_init($apiUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_CONNECTTIMEOUT => 5
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$curlErr = curl_error($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlErr) {
|
||||
error_log("[SiroGeminiService] Curl Error: $curlErr");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
error_log("[SiroGeminiService] HTTP Error $httpCode. Response: $response");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$responseData = json_decode($response, true);
|
||||
$rawText = $responseData['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
||||
// تنظيف أي علامات كود ماركداون محتملة من الموديل
|
||||
$cleanJson = trim(preg_replace('/```json|```/', '', $rawText));
|
||||
|
||||
$decoded = json_decode($cleanJson, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
error_log("[SiroGeminiService] JSON Decode Error: " . json_last_error_msg() . " | Raw text: $rawText");
|
||||
return null;
|
||||
}
|
||||
return $decoded;
|
||||
} catch (Exception $e) {
|
||||
error_log("[SiroGeminiService] Exception: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,8 @@ function getAllowedSocketUrls(): array {
|
||||
}
|
||||
// القيم الافتراضية لو لم تكن موجودة في .env
|
||||
return [
|
||||
'http://188.68.36.205:2021',
|
||||
'http://188.68.36.205:3031',
|
||||
'http://location.intaleq.xyz:2021',
|
||||
'http://location.intaleq.xyz:3031',
|
||||
'https://location.intaleq.xyz',
|
||||
];
|
||||
}
|
||||
@@ -44,7 +44,7 @@ function isAllowedSocketUrl(string $url): bool {
|
||||
}
|
||||
|
||||
function sendToLocationServer($action, $data) {
|
||||
$url = getenv('LOCATION_SERVER_URL') ?: 'http://188.68.36.205:2021';
|
||||
$url = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
|
||||
if (!isAllowedSocketUrl($url)) {
|
||||
error_log("[SSRF_BLOCKED] Attempted connection to: $url");
|
||||
return;
|
||||
@@ -67,7 +67,7 @@ function sendToLocationServer($action, $data) {
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
function findBestDrivers($con, $lat, $lng, $carType) {
|
||||
function findBestDrivers($con, $lat, $lng, $carType, $endLat = null, $endLng = null) {
|
||||
// 1. الاتصال بـ Redis لجلب الأقرب
|
||||
$locationServerUrl = "https://location.intaleq.xyz/api_get_nearby.php";
|
||||
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
||||
@@ -108,16 +108,20 @@ function findBestDrivers($con, $lat, $lng, $carType) {
|
||||
// تعريف الثوابت
|
||||
$CAT_CAR = 1; $CAT_BIKE = 2; $CAT_VAN = 3; $FUEL_ELECTRIC = 3;
|
||||
|
||||
// 3. الاستعلام (بدون platform)
|
||||
// 3. الاستعلام (دمج جدول وجهات السائقين dd للتحقق من الملاءمة)
|
||||
$sql = "SELECT
|
||||
d.id AS driver_id,
|
||||
dt.token,
|
||||
cr.year,
|
||||
cr.vehicle_category_id,
|
||||
d.gender
|
||||
d.gender,
|
||||
dd.target_latitude,
|
||||
dd.target_longitude,
|
||||
dd.is_active AS has_destination
|
||||
FROM driver d
|
||||
JOIN CarRegistration cr ON cr.driverID = d.id
|
||||
JOIN driverToken dt ON dt.captain_id = d.id
|
||||
LEFT JOIN driver_destinations dd ON dd.driver_id = d.id AND dd.is_active = 1 AND dd.usage_date = CURDATE()
|
||||
WHERE d.id IN ($placeholders) ";
|
||||
|
||||
// ✅ FIX C-01: استخدام allowlist للـ carType لمنع SQL Injection
|
||||
@@ -150,10 +154,9 @@ function findBestDrivers($con, $lat, $lng, $carType) {
|
||||
$sqlParams[] = $FUEL_ELECTRIC;
|
||||
break;
|
||||
case 'Lady':
|
||||
$femaleHash = 'bQ6yWJ2EVXKZooHdGclvmFiDlZCM8UYeO+ILFjDUvpQ=';
|
||||
$sql .= " AND cr.vehicle_category_id = ? AND d.gender = ? ";
|
||||
$sqlParams[] = $CAT_CAR;
|
||||
$sqlParams[] = $femaleHash;
|
||||
$sqlParams[] = getenv('FEMALE_GENDER_HASH') ?: '';
|
||||
break;
|
||||
case 'Van':
|
||||
$sql .= " AND cr.vehicle_category_id = ? ";
|
||||
@@ -180,9 +183,27 @@ function findBestDrivers($con, $lat, $lng, $carType) {
|
||||
$stmt->execute($allParams);
|
||||
$finalDrivers = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// دمج البيانات
|
||||
foreach ($finalDrivers as &$driver) {
|
||||
$filteredDrivers = [];
|
||||
|
||||
// دمج البيانات وتطبيق تصفية الوجهة
|
||||
foreach ($finalDrivers as $driver) {
|
||||
$did = $driver['driver_id'];
|
||||
|
||||
// تحقق من توافق الوجهة إذا كان السائق قد حدد وجهة والرحلة تملك إحداثيات نهاية
|
||||
if ($driver['has_destination'] && $endLat !== null && $endLng !== null) {
|
||||
$driverDestLat = (float)$driver['target_latitude'];
|
||||
$driverDestLng = (float)$driver['target_longitude'];
|
||||
|
||||
// حساب المسافة بين وجهة السائق ووجهة الرحلة
|
||||
$destDistance = getDistanceBetweenPoints((float)$endLat, (float)$endLng, $driverDestLat, $driverDestLng);
|
||||
|
||||
// إذا كانت المسافة أكبر من 5.0 كم، نستبعد السائق لأن وجهته لا تطابق مسار الرحلة
|
||||
if ($destDistance > 5.0) {
|
||||
error_log("[findBestDrivers] Filtering out driver $did because destination gap is $destDistance km (> 5km)");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($redisMap[$did])) {
|
||||
$driver['distance_km'] = $redisMap[$did]['distance'];
|
||||
$driver['lat'] = $redisMap[$did]['lat'];
|
||||
@@ -190,22 +211,34 @@ function findBestDrivers($con, $lat, $lng, $carType) {
|
||||
} else {
|
||||
$driver['distance_km'] = 999;
|
||||
}
|
||||
|
||||
$filteredDrivers[] = $driver;
|
||||
}
|
||||
|
||||
// الترتيب
|
||||
usort($finalDrivers, function($a, $b) {
|
||||
usort($filteredDrivers, function($a, $b) {
|
||||
return $a['distance_km'] <=> $b['distance_km'];
|
||||
});
|
||||
|
||||
return array_slice($finalDrivers, 0, 30);
|
||||
return array_slice($filteredDrivers, 0, 30);
|
||||
} catch (Exception $e) {
|
||||
error_log("FindBestDrivers Error: " . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// دالة مساعدة لحساب المسافة بين نقطتين جغرافيين
|
||||
function getDistanceBetweenPoints($lat1, $lon1, $lat2, $lon2) {
|
||||
$theta = $lon1 - $lon2;
|
||||
$dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta));
|
||||
$dist = acos(max(-1.0, min(1.0, $dist))); // لمنع أخطاء تجاوز نطاق acos
|
||||
$dist = rad2deg($dist);
|
||||
$miles = $dist * 60 * 1.1515;
|
||||
return ($miles * 1.609344); // إرجاع المسافة بالكيلومتر
|
||||
}
|
||||
// --- دالة مساعدة لمخاطبة سيرفر السائقين (Location Socket) ---
|
||||
function notifyDriversRideTaken($rideId, $winnerDriverId) {
|
||||
$url = "http://188.68.36.205:2021";
|
||||
$url = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
|
||||
if (!isAllowedSocketUrl($url)) return;
|
||||
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
||||
|
||||
@@ -227,7 +260,7 @@ function notifyDriversRideTaken($rideId, $winnerDriverId) {
|
||||
curl_close($ch);
|
||||
}
|
||||
function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = null) {
|
||||
$url = "http://188.68.36.205:2021";
|
||||
$url = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
|
||||
if (!isAllowedSocketUrl($url)) return null;
|
||||
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
||||
|
||||
@@ -263,7 +296,7 @@ function notifyDriversOnLocationServer($drivers_ids_array, $payload, $rideId = n
|
||||
* تخاطب السوكيت الموجود محلياً على نفس السيرفر
|
||||
*/
|
||||
function notifyPassengerOnRideServer($passenger_id, $payload) {
|
||||
$url = "http://188.68.36.205:3031";
|
||||
$url = getenv('RIDE_SOCKET_URL') ?: 'http://location.intaleq.xyz:3031';
|
||||
if (!isAllowedSocketUrl($url)) return null;
|
||||
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
||||
|
||||
@@ -310,7 +343,7 @@ function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNa
|
||||
$countDrivers = count($driversData);
|
||||
error_log("🚀 [DISPATCH_START] RideID: $rideId | Drivers Count: $countDrivers");
|
||||
|
||||
$socketUrl = 'http://188.68.36.205:2021';
|
||||
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
|
||||
if (!isAllowedSocketUrl($socketUrl)) return;
|
||||
$internalKey = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
|
||||
|
||||
|
||||
@@ -4073,7 +4073,7 @@ try {
|
||||
```
|
||||
-- MySQL dump 10.13 Distrib 8.0.36-28, for Linux (x86_64)
|
||||
--
|
||||
-- Host: 188.68.36.205 Database: locationDB
|
||||
-- Host: <server-ip> Database: locationDB
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 8.0.36-28
|
||||
|
||||
@@ -18777,7 +18777,7 @@ try {
|
||||
if ($driverId > 0) {
|
||||
|
||||
// أ) Socket (إشعار السائق في التطبيق فوراً)
|
||||
$socketUrl = 'http://188.68.36.205:2021';
|
||||
$socketUrl = 'http://<server-ip>:2021';
|
||||
$internalKeyPath = '/home/siro-api/.internal_socket_key';
|
||||
$internalKey = file_exists($internalKeyPath) ? trim(file_get_contents($internalKeyPath)) : '';
|
||||
|
||||
@@ -20496,7 +20496,7 @@ try {
|
||||
<?php
|
||||
// test_socket_dispatch.php
|
||||
|
||||
$socketUrl = "http://188.68.36.205:2021";
|
||||
$socketUrl = "http://<server-ip>:2021";
|
||||
$INTERNAL_KEY = trim(file_get_contents('/home/siro-api/.internal_socket_key'));
|
||||
|
||||
// جرّب Driver ID موجود عندك
|
||||
|
||||
101
backend/ride/location/save_driver_destination.php
Normal file
101
backend/ride/location/save_driver_destination.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// ride/location/save_driver_destination.php
|
||||
// API Endpoint for Captains to set their destination (max 2 times daily)
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// 1. Authorize Driver
|
||||
if ($role !== 'driver') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized. Driver role required.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Filter Inputs
|
||||
$action = filterRequest('action') ?? 'set';
|
||||
$destLat = filterRequest('destination_lat') ?? filterRequest('target_latitude');
|
||||
$destLng = filterRequest('destination_lng') ?? filterRequest('target_longitude');
|
||||
$destName = filterRequest('destination_name') ?? 'Destination';
|
||||
|
||||
try {
|
||||
if ($action === 'get') {
|
||||
$stmtGet = $con->prepare("
|
||||
SELECT target_latitude, target_longitude, destination_name, created_at
|
||||
FROM driver_destinations
|
||||
WHERE driver_id = :did
|
||||
AND is_active = 1
|
||||
LIMIT 1
|
||||
");
|
||||
$stmtGet->execute([':did' => $user_id]);
|
||||
$activeDest = $stmtGet->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($activeDest) {
|
||||
jsonSuccess($activeDest, "Active destination retrieved.");
|
||||
} else {
|
||||
jsonSuccess(null, "No active destination set.");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'clear') {
|
||||
$stmtDeactivate = $con->prepare("
|
||||
UPDATE driver_destinations
|
||||
SET is_active = 0
|
||||
WHERE driver_id = :did
|
||||
AND is_active = 1
|
||||
");
|
||||
$stmtDeactivate->execute([':did' => $user_id]);
|
||||
jsonSuccess(null, "تم إلغاء تفعيل الوجهة الشخصية بنجاح.");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Default action: set
|
||||
if (empty($destLat) || empty($destLng)) {
|
||||
jsonError("Missing required parameters: destination_lat and destination_lng are required.");
|
||||
}
|
||||
|
||||
// 3. Enforce Limit: Max 2 times daily
|
||||
$stmtCount = $con->prepare("
|
||||
SELECT COUNT(*)
|
||||
FROM driver_destinations
|
||||
WHERE driver_id = :did
|
||||
AND usage_date = CURDATE()
|
||||
AND is_active = 1
|
||||
");
|
||||
$stmtCount->execute([':did' => $user_id]);
|
||||
$dailyCount = intval($stmtCount->fetchColumn());
|
||||
|
||||
if ($dailyCount >= 2) {
|
||||
jsonError("حسناً كابتن، لقد وصلت للحد الأقصى المسموح به لتحديد الوجهة اليوم (مرتان في اليوم).");
|
||||
}
|
||||
|
||||
// 4. Deactivate previous active destinations for this driver
|
||||
$stmtDeactivate = $con->prepare("
|
||||
UPDATE driver_destinations
|
||||
SET is_active = 0
|
||||
WHERE driver_id = :did
|
||||
AND is_active = 1
|
||||
");
|
||||
$stmtDeactivate->execute([':did' => $user_id]);
|
||||
|
||||
// 5. Insert new destination
|
||||
$stmtInsert = $con->prepare("
|
||||
INSERT INTO driver_destinations
|
||||
(driver_id, target_latitude, target_longitude, destination_name, is_active, usage_date)
|
||||
VALUES (:did, :lat, :lng, :name, 1, CURDATE())
|
||||
");
|
||||
$stmtInsert->execute([
|
||||
':did' => $user_id,
|
||||
':lat' => (float)$destLat,
|
||||
':lng' => (float)$destLng,
|
||||
':name' => $destName
|
||||
]);
|
||||
|
||||
jsonSuccess(null, "تم حفظ وجهتك كابتن بنجاح! سيتم توجيه الطلبات المطابقة لوجهتك.");
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("[save_driver_destination.php] Error: " . $e->getMessage());
|
||||
jsonError("Failed to save driver destination: " . $e->getMessage());
|
||||
}
|
||||
@@ -109,12 +109,13 @@ function getPerKmRate($carType, $kazanRow) {
|
||||
}
|
||||
|
||||
function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanRow, $startNameAddress, $endNameAddress, $destLat, $destLng, $passengerLat, $passengerLng, $carType = 'Speed') {
|
||||
global $redis, $redisLocation;
|
||||
global $redis, $redisLocation, $con;
|
||||
|
||||
$surgeMultiplier = 1.0;
|
||||
if (isset($redis) && $redis !== null) {
|
||||
try {
|
||||
$grid_size = 0.0135;
|
||||
$refLat = 33.5;
|
||||
$grid_size = 0.0135 * (cos(deg2rad($refLat)) / max(cos(deg2rad((float)$passengerLat)), 0.01));
|
||||
$grid_lat = round((float)$passengerLat / $grid_size) * $grid_size;
|
||||
$grid_lng = round((float)$passengerLng / $grid_size) * $grid_size;
|
||||
$grid_id = $grid_lat . "_" . $grid_lng;
|
||||
@@ -242,6 +243,105 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR
|
||||
|
||||
$price = max($fare, $minFare);
|
||||
|
||||
// Apply competitor-linked regional pricing overrides (undercut by 8%)
|
||||
$competitorTarget = null;
|
||||
if (isset($con) && $con !== null) {
|
||||
try {
|
||||
$countryCodeMap = [
|
||||
'Syria' => 'SY',
|
||||
'Jordan' => 'JO',
|
||||
'Egypt' => 'EG',
|
||||
'Iraq' => 'IQ'
|
||||
];
|
||||
$cc = $countryCodeMap[$country] ?? 'SY';
|
||||
|
||||
$latDelta = 0.02;
|
||||
$lngDelta = 0.02;
|
||||
|
||||
$minFlat = $passengerLat - $latDelta;
|
||||
$maxFlat = $passengerLat + $latDelta;
|
||||
$minFlng = $passengerLng - $lngDelta;
|
||||
$maxFlng = $passengerLng + $lngDelta;
|
||||
|
||||
$minTlat = $destLat - $latDelta;
|
||||
$maxTlat = $destLat + $latDelta;
|
||||
$minTlng = $destLng - $lngDelta;
|
||||
$maxTlng = $destLng + $lngDelta;
|
||||
|
||||
// Layer 1: Start and End match within bounding box
|
||||
$sqlComp = "SELECT total_price, distance_km
|
||||
FROM competitor_prices
|
||||
WHERE country_code = :country_code
|
||||
AND (from_latitude + 0.0) BETWEEN :min_flat AND :max_flat
|
||||
AND (from_longitude + 0.0) BETWEEN :min_flng AND :max_flng
|
||||
AND (to_latitude + 0.0) BETWEEN :min_tlat AND :max_tlat
|
||||
AND (to_longitude + 0.0) BETWEEN :min_tlng AND :max_tlng
|
||||
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
ORDER BY created_at DESC LIMIT 5";
|
||||
$stmtComp = $con->prepare($sqlComp);
|
||||
$stmtComp->execute([
|
||||
':country_code' => $cc,
|
||||
':min_flat' => $minFlat,
|
||||
':max_flat' => $maxFlat,
|
||||
':min_flng' => $minFlng,
|
||||
':max_flng' => $maxFlng,
|
||||
':min_tlat' => $minTlat,
|
||||
':max_tlat' => $maxTlat,
|
||||
':min_tlng' => $minTlng,
|
||||
':max_tlng' => $maxTlng
|
||||
]);
|
||||
$matches = $stmtComp->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($matches)) {
|
||||
// Layer 2 Fallback: Start match only within bounding box
|
||||
$sqlFallback = "SELECT total_price, distance_km
|
||||
FROM competitor_prices
|
||||
WHERE country_code = :country_code
|
||||
AND (from_latitude + 0.0) BETWEEN :min_flat AND :max_flat
|
||||
AND (from_longitude + 0.0) BETWEEN :min_flng AND :max_flng
|
||||
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||
ORDER BY created_at DESC LIMIT 10";
|
||||
$stmtFallback = $con->prepare($sqlFallback);
|
||||
$stmtFallback->execute([
|
||||
':country_code' => $cc,
|
||||
':min_flat' => $minFlat,
|
||||
':max_flat' => $maxFlat,
|
||||
':min_flng' => $minFlng,
|
||||
':max_flng' => $maxFlng
|
||||
]);
|
||||
$matches = $stmtFallback->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
if (!empty($matches)) {
|
||||
$normalizedPrices = [];
|
||||
foreach ($matches as $row) {
|
||||
$compDist = (float)$row['distance_km'];
|
||||
$compPrice = (float)$row['total_price'];
|
||||
if ($compDist > 0) {
|
||||
$normalizedPrices[] = ($compPrice / $compDist) * $distance;
|
||||
}
|
||||
}
|
||||
if (!empty($normalizedPrices)) {
|
||||
$competitorTarget = array_sum($normalizedPrices) / count($normalizedPrices);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("[calculateDynamicPrice] Competitor pricing query failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($competitorTarget !== null) {
|
||||
$undercutPrice = $competitorTarget * 0.92;
|
||||
$speedBaseRate = getPerKmRate('Speed', $kazanRow);
|
||||
$currentBaseRate = getPerKmRate($carType, $kazanRow);
|
||||
$categoryMultiplier = $speedBaseRate > 0 ? ($currentBaseRate / $speedBaseRate) : 1.0;
|
||||
|
||||
$targetAdjustedPrice = $undercutPrice * $categoryMultiplier;
|
||||
if ($price > $targetAdjustedPrice) {
|
||||
$price = $targetAdjustedPrice;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply kazan (e.g. 11%)
|
||||
$withCommission = ceil($price * (1 + $kazanPercent / 100));
|
||||
$kazan = $withCommission - $price;
|
||||
@@ -341,7 +441,7 @@ if (isset($encryptionHelper)) {
|
||||
// ✅ FIX R6: تضمين distance و duration في الـ token لمنع التلاعب
|
||||
'distance' => $distance,
|
||||
'duration' => $duration,
|
||||
'expires' => time() + 180, // Valid for 3 minutes
|
||||
'expires' => time() + 420, // Valid for 7 minutes
|
||||
'prices' => $pricesRaw
|
||||
];
|
||||
$priceToken = $encryptionHelper->encryptData(json_encode($tokenPayload));
|
||||
|
||||
@@ -281,7 +281,7 @@ try {
|
||||
];
|
||||
|
||||
// Direct dispatch للسائقين القريبين
|
||||
$driversData = findBestDrivers($con, $startLat, $startLng, $carType);
|
||||
$driversData = findBestDrivers($con, $startLat, $startLng, $carType, $endLat, $endLng);
|
||||
if (!empty($driversData)) {
|
||||
dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper);
|
||||
error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers.");
|
||||
|
||||
@@ -82,7 +82,7 @@ try {
|
||||
if ($driverId > 0) {
|
||||
|
||||
// أ) Socket (إشعار السائق في التطبيق فوراً)
|
||||
$socketUrl = 'http://188.68.36.205:2021';
|
||||
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
|
||||
$internalKeyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '';
|
||||
$internalKey = ($internalKeyPath && file_exists($internalKeyPath)) ? trim(file_get_contents($internalKeyPath)) : (getenv('INTERNAL_SOCKET_KEY') ?: '');
|
||||
|
||||
|
||||
@@ -93,11 +93,11 @@ try {
|
||||
$latVal = doubleval($startLat);
|
||||
$lngVal = doubleval($startLng);
|
||||
|
||||
$driversData = findBestDrivers($con, $con_tracking, $latVal, $lngVal, $carType);
|
||||
$driversData = findBestDrivers($con, $latVal, $lngVal, $carType, $endLat, $endLng);
|
||||
|
||||
if (!empty($driversData)) {
|
||||
// استدعاء دالة الإرسال الموحدة (الموجودة في functions.php)
|
||||
dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startName);
|
||||
dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startName, $encryptionHelper);
|
||||
}
|
||||
|
||||
jsonSuccess(null, "Ride reset and resent to drivers");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
// test_socket_dispatch.php
|
||||
|
||||
$socketUrl = "http://188.68.36.205:2021";
|
||||
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
|
||||
$INTERNAL_KEY = getenv('INTERNAL_SOCKET_KEY');
|
||||
if (empty($INTERNAL_KEY)) {
|
||||
$keyPath = getenv('INTERNAL_SOCKET_KEY_PATH');
|
||||
|
||||
@@ -1125,12 +1125,54 @@ CREATE TABLE `passenger_opening_locations` (
|
||||
`passenger_id` varchar(100) NOT NULL,
|
||||
`latitude` varchar(30) NOT NULL,
|
||||
`longitude` varchar(30) NOT NULL,
|
||||
`country_code` varchar(5) NOT NULL DEFAULT 'SY',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`location_point` point NOT NULL /*!80003 SRID 4326 */,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_passenger_id` (`passenger_id`)
|
||||
KEY `idx_passenger_id` (`passenger_id`),
|
||||
KEY `idx_country` (`country_code`),
|
||||
SPATIAL KEY `idx_location_point` (`location_point`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
/*!50003 SET @saved_cs_client = @@character_set_client */ ;
|
||||
/*!50003 SET @saved_cs_results = @@character_set_results */ ;
|
||||
/*!50003 SET @saved_col_connection = @@collation_connection */ ;
|
||||
/*!50003 SET character_set_client = utf8mb4 */ ;
|
||||
/*!50003 SET character_set_results = utf8mb4 */ ;
|
||||
/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ;
|
||||
/*!50003 SET @saved_sql_mode = @@sql_mode */ ;
|
||||
/*!50003 SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO' */ ;
|
||||
DELIMITER ;;
|
||||
/*!50003 CREATE*/ /*!50017 DEFINER=`siroUserDB1`@`%`*/ /*!50003 TRIGGER `trg_before_insert_passenger_opening_locations` BEFORE INSERT ON `passenger_opening_locations` FOR EACH ROW BEGIN
|
||||
SET NEW.location_point = ST_PointFromText(CONCAT('POINT(', NEW.longitude, ' ', NEW.latitude, ')'), 4326);
|
||||
END */;;
|
||||
DELIMITER ;
|
||||
/*!50003 SET sql_mode = @saved_sql_mode */ ;
|
||||
/*!50003 SET character_set_client = @saved_cs_client */ ;
|
||||
/*!50003 SET character_set_results = @saved_cs_results */ ;
|
||||
/*!50003 SET collation_connection = @saved_col_connection */ ;
|
||||
|
||||
/*!50003 SET @saved_cs_client = @@character_set_client */ ;
|
||||
/*!50003 SET @saved_cs_results = @@character_set_results */ ;
|
||||
/*!50003 SET @saved_col_connection = @@collation_connection */ ;
|
||||
/*!50003 SET character_set_client = utf8mb4 */ ;
|
||||
/*!50003 SET character_set_results = utf8mb4 */ ;
|
||||
/*!50003 SET collation_connection = utf8mb4_0900_ai_ci */ ;
|
||||
/*!50003 SET @saved_sql_mode = @@sql_mode */ ;
|
||||
/*!50003 SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO' */ ;
|
||||
DELIMITER ;;
|
||||
/*!50003 CREATE*/ /*!50017 DEFINER=`siroUserDB1`@`%`*/ /*!50003 TRIGGER `trg_before_update_passenger_opening_locations` BEFORE UPDATE ON `passenger_opening_locations` FOR EACH ROW BEGIN
|
||||
IF NEW.latitude <> OLD.latitude OR NEW.longitude <> OLD.longitude THEN
|
||||
SET NEW.location_point = ST_PointFromText(CONCAT('POINT(', NEW.longitude, ' ', NEW.latitude, ')'), 4326);
|
||||
END IF;
|
||||
END */;;
|
||||
DELIMITER ;
|
||||
/*!50003 SET sql_mode = @saved_sql_mode */ ;
|
||||
/*!50003 SET character_set_client = @saved_cs_client */ ;
|
||||
/*!50003 SET character_set_results = @saved_cs_results */ ;
|
||||
/*!50003 SET collation_connection = @saved_col_connection */ ;
|
||||
|
||||
--
|
||||
-- Table structure for table `passengers`
|
||||
--
|
||||
@@ -1302,10 +1344,11 @@ CREATE TABLE `promos` (
|
||||
`amount` varchar(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0',
|
||||
`description` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
|
||||
`passengerID` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none',
|
||||
`source` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'manual',
|
||||
`validity_start_date` date DEFAULT NULL,
|
||||
`validity_end_date` date DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `passengerID` (`passengerID`)
|
||||
KEY `passengerID` (`passengerID`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=637 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
@@ -1813,3 +1856,72 @@ CREATE TABLE IF NOT EXISTS `driver_cash_claims` (
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
--
|
||||
-- Table structure for table `competitor_prices`
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS `competitor_prices` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`competitor_name` varchar(50) NOT NULL,
|
||||
`from_latitude` varchar(30) NOT NULL,
|
||||
`from_longitude` varchar(30) NOT NULL,
|
||||
`to_latitude` varchar(30) NOT NULL,
|
||||
`to_longitude` varchar(30) NOT NULL,
|
||||
`distance_km` decimal(8,2) NOT NULL,
|
||||
`total_price` decimal(10,2) NOT NULL,
|
||||
`price_per_km` decimal(8,2) NOT NULL,
|
||||
`country_code` varchar(5) NOT NULL DEFAULT 'SY',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_competitor_country` (`competitor_name`, `country_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
--
|
||||
-- Table structure for table `marketing_campaigns_log`
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS `marketing_campaigns_log` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`passenger_id` varchar(100) NOT NULL,
|
||||
`message_type` enum('sms','whatsapp','push') NOT NULL,
|
||||
`country_code` varchar(5) NOT NULL,
|
||||
`region_name` varchar(100) DEFAULT NULL,
|
||||
`triggered_by` enum('anomaly','manual','autopilot') NOT NULL DEFAULT 'autopilot',
|
||||
`sent_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`opened_app_after` tinyint(1) DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_passenger_date` (`passenger_id`, `sent_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
--
|
||||
-- Table structure for table `price_anomalies`
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS `price_anomalies` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`country_code` varchar(5) NOT NULL,
|
||||
`region_name` varchar(100) NOT NULL,
|
||||
`anomaly_type` enum('surge','undercut','opportunity') NOT NULL,
|
||||
`competitor_name` varchar(50) NOT NULL,
|
||||
`our_price` decimal(10,2) NOT NULL,
|
||||
`competitor_price` decimal(10,2) NOT NULL,
|
||||
`price_gap_percent` decimal(5,2) NOT NULL,
|
||||
`action_taken` enum('notification_sent','price_adjusted','ignored') NOT NULL DEFAULT 'ignored',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_country_created` (`country_code`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
--
|
||||
-- Table structure for table `driver_destinations`
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS `driver_destinations` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`driver_id` varchar(100) NOT NULL,
|
||||
`target_latitude` decimal(10,7) NOT NULL,
|
||||
`target_longitude` decimal(10,7) NOT NULL,
|
||||
`destination_name` varchar(150) DEFAULT NULL,
|
||||
`is_active` tinyint(1) DEFAULT 1,
|
||||
`usage_date` date NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_driver_date` (`driver_id`, `usage_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- MySQL dump 10.13 Distrib 8.0.36-28, for Linux (x86_64)
|
||||
--
|
||||
-- Host: 188.68.36.205 Database: locationDB
|
||||
-- Host: <db-host> Database: locationDB
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 8.0.36-28
|
||||
|
||||
|
||||
Reference in New Issue
Block a user