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>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
+248
View File
@@ -0,0 +1,248 @@
<?php
// ============================================================
// core/Services/FcmService.php
// إرسال FCM مع كاش توكن في Redis (بدل ملف)
// ============================================================
class FcmService
{
private ?Redis $redis;
private string $serviceAccountFile;
public function __construct(?Redis $redis = null)
{
$this->redis = $redis;
$envPath = getenv('FIREBASE_SERVICE_ACCOUNT_PATH') ?: '';
if (!empty($envPath) && file_exists($envPath)) {
$this->serviceAccountFile = $envPath;
} elseif (file_exists('/keys/firebase_service_account.json')) {
$this->serviceAccountFile = '/keys/firebase_service_account.json';
} elseif (file_exists('/keys/service-account.json')) {
$this->serviceAccountFile = '/keys/service-account.json';
} elseif (file_exists(__DIR__ . '/../../keys/firebase_service_account.json')) {
$this->serviceAccountFile = __DIR__ . '/../../keys/firebase_service_account.json';
} elseif (file_exists(__DIR__ . '/../../ride/firebase/service-account.json')) {
$this->serviceAccountFile = __DIR__ . '/../../ride/firebase/service-account.json';
} else {
$this->serviceAccountFile = __DIR__ . '/../../keys/firebase_service_account.json'; // Default fallback
}
}
// ── إرسال إشعار ────────────────────────────────────────
public function send(
string $token,
string $title,
string $body,
array $data = [],
string $category = 'Order',
string $tone = 'ding'
): array {
$accessToken = $this->getAccessToken();
if (!$accessToken) {
return ['status' => 'error', 'message' => 'No access token'];
}
if (!file_exists($this->serviceAccountFile)) {
return ['status' => 'error', 'message' => 'Service account file missing'];
}
$creds = json_decode(file_get_contents($this->serviceAccountFile), true);
$projectId = $creds['project_id'];
$fcmUrl = "https://fcm.googleapis.com/v1/projects/$projectId/messages:send";
$finalData = array_merge($data, [
'title' => $title,
'body' => $body,
'tone' => $tone,
'category' => $category,
'type' => $category,
]);
// FCM يشترط أن تكون كل القيم strings
$processedData = array_map(
fn($v) => is_array($v) || is_object($v)
? json_encode($v, JSON_UNESCAPED_UNICODE)
: (string)$v,
$finalData
);
$payload = [
'message' => [
'token' => $token,
'data' => $processedData,
'android' => [
'priority' => 'HIGH',
'notification' => [
'sound' => $tone === 'ding' ? 'default' : $tone,
'channel_id' => 'high_importance_channel'
]
],
],
];
if (!empty($title) && !empty($body)) {
$payload['message']['notification'] = [
'title' => $title,
'body' => $body,
];
$iosSound = $tone === 'ding' || $tone === 'default' ? 'default' : (str_ends_with($tone, '.caf') ? $tone : $tone . '.caf');
$payload['message']['apns'] = [
'payload' => [
'aps' => [
'sound' => $iosSound
]
]
];
} else {
$payload['message']['apns'] = [
'headers' => [
'apns-priority' => '5',
'apns-push-type' => 'background'
],
'payload' => [
'aps' => [
'content-available' => 1
]
]
];
}
$ch = curl_init($fcmUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $accessToken",
'Content-Type: application/json; charset=UTF-8',
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_FRESH_CONNECT => false, // إعادة استخدام الاتصال
CURLOPT_FORBID_REUSE => false,
CURLOPT_TCP_KEEPALIVE => 1,
]);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_errno($ch);
curl_close($ch);
error_log("[FCM_DEBUG] Token: " . substr($token, 0, 10) . "... Payload: " . json_encode($payload, JSON_UNESCAPED_UNICODE) . " | Result: $httpCode - $result");
if ($curlErr) {
return ['status' => 'error', 'message' => 'CURL error'];
}
return $httpCode === 200
? ['status' => 'success']
: ['status' => 'error', 'code' => $httpCode, 'response' => $result];
}
// ── إرسال إشعار لـ FCM Topic (قناة المواصلاتي وغيرها) ──
public function sendToTopic(
string $topic,
string $title,
string $body,
array $data = []
): array {
$accessToken = $this->getAccessToken();
if (!$accessToken) return ['status' => 'error', 'message' => 'No access token'];
if (!file_exists($this->serviceAccountFile)) {
return ['status' => 'error', 'message' => 'Service account file missing'];
}
$creds = json_decode(file_get_contents($this->serviceAccountFile), true);
$projectId = $creds['project_id'];
$fcmUrl = "https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send";
$processedData = array_map(
fn($v) => is_array($v) || is_object($v) ? json_encode($v, JSON_UNESCAPED_UNICODE) : (string)$v,
array_merge($data, ['title' => $title, 'body' => $body])
);
$payload = [
'message' => [
'topic' => $topic,
'notification' => ['title' => $title, 'body' => $body],
'data' => $processedData,
'android' => ['priority' => 'HIGH'],
],
];
$ch = curl_init($fcmUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $accessToken", 'Content-Type: application/json; charset=UTF-8'],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode === 200
? ['status' => 'success']
: ['status' => 'error', 'code' => $httpCode, 'response' => $result];
}
// ── Access Token مع Redis Cache ─────────────────────────
private function getAccessToken(): ?string
{
// 1. من Redis
if ($this->redis) {
$cached = $this->redis->get('google_fcm_access_token');
if ($cached) return $cached;
}
// 2. طلب جديد
$token = $this->fetchGoogleToken();
if ($token && $this->redis) {
$this->redis->setex('google_fcm_access_token', 3500, $token);
}
return $token;
}
private function fetchGoogleToken(): ?string
{
if (!file_exists($this->serviceAccountFile)) return null;
$creds = json_decode(file_get_contents($this->serviceAccountFile), true);
$clientEmail = $creds['client_email'];
$privateKey = $creds['private_key'];
$now = time();
$header = rtrim(strtr(base64_encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT'])), '+/', '-_'), '=');
$claim = rtrim(strtr(base64_encode(json_encode([
'iss' => $clientEmail,
'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
'aud' => 'https://oauth2.googleapis.com/token',
'exp' => $now + 3600,
'iat' => $now,
])), '+/', '-_'), '=');
$signature = '';
openssl_sign("$header.$claim", $signature, $privateKey, 'SHA256');
$jwt = "$header.$claim." . rtrim(strtr(base64_encode($signature), '+/', '-_'), '=');
$ch = curl_init('https://oauth2.googleapis.com/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt,
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$res = curl_exec($ch);
curl_close($ch);
return json_decode($res, true)['access_token'] ?? null;
}
}
@@ -0,0 +1,163 @@
<?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);
}
}
?>
+78
View File
@@ -0,0 +1,78 @@
<?php
// ============================================================
// core/Services/OtpService.php
// تخزين OTP في Redis بدلاً من MySQL (أسرع وأخف)
// ============================================================
class OtpService
{
private ?Redis $redis;
private const OTP_TTL = 300; // 5 دقائق
private const MAX_ATTEMPTS = 3;
private const LOCKOUT_TTL = 1800; // 30 دقيقة إذا تجاوز المحاولات
public function __construct(?Redis $redis)
{
$this->redis = $redis;
}
// ── توليد وحفظ OTP ─────────────────────────────────────
// $digits: عدد الأرقام — الافتراضي 6، يمكن تمرير 3 لـ transit (100–999)
public function generate(string $phone, int $digits = 3): string
{
$min = (int)str_pad('1', $digits, '0'); // digits=3 → 100 | digits=6 → 100000
$max = (int)str_pad('9', $digits, '9'); // digits=3 → 999 | digits=6 → 999999
$otp = str_pad((string)random_int($min, $max), $digits, '0', STR_PAD_LEFT);
if ($this->redis) {
$key = "otp:{$phone}";
$this->redis->setex($key, self::OTP_TTL, password_hash($otp, PASSWORD_BCRYPT));
$this->redis->del("otp:attempts:{$phone}");
}
return $otp;
}
// ── التحقق من OTP ───────────────────────────────────────
public function verify(string $phone, string $inputOtp): bool
{
if (!$this->redis) return false;
// فحص الـ lockout
if ($this->redis->exists("otp:locked:{$phone}")) {
return false;
}
$key = "otp:{$phone}";
$stored = $this->redis->get($key);
if (!$stored) {
return false; // انتهت صلاحية الـ OTP
}
$attemptsKey = "otp:attempts:{$phone}";
if (!password_verify($inputOtp, $stored)) {
$attempts = $this->redis->incr($attemptsKey);
$this->redis->expire($attemptsKey, self::OTP_TTL);
if ($attempts >= self::MAX_ATTEMPTS) {
// قفل لمدة 30 دقيقة
$this->redis->setex("otp:locked:{$phone}", self::LOCKOUT_TTL, '1');
$this->redis->del($key);
}
return false;
}
// نجح التحقق — احذف الـ OTP
$this->redis->del($key);
$this->redis->del($attemptsKey);
return true;
}
// ── فحص هل الرقم مقفل ──────────────────────────────────
public function isLocked(string $phone): bool
{
return $this->redis && (bool)$this->redis->exists("otp:locked:{$phone}");
}
}
+379
View File
@@ -0,0 +1,379 @@
<?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-flash-lite-latest'
): ?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\": \"عنوان الإشعار (بحد أقصى 5 كلمات)\",
\"push_body\": \"محتوى الإشعار القصير والمثير للاهتمام (بحد أقصى 15 كلمة)\",
\"sms_body\": \"محتوى رسالة الاستعادة (بحد أقصى 20 كلمة)\"
}
";
return $this->callGemini($prompt, $model);
}
/**
* يُولّد إشعاراً مخصصاً لحدث دخول الجيوفينس بناءً على بيانات أسعار حقيقية.
*
* @param string $zoneName اسم منطقة السياج الجغرافي
* @param string $countryCode رمز الدولة (SY, JO, EG, IQ)
* @param float $savingsPct نسبة التوفير (مثال: 8.5)
* @param string $topCompetitor اسم أبرز منافس في المنطقة
* @param string $model
* @return array|null
*/
public function generateGeofenceMessage(
string $zoneName,
string $countryCode,
float $savingsPct,
string $topCompetitor = 'كريم',
string $model = 'gemini-flash-lite-latest'
): ?array {
if (!$this->apiKey) return null;
$dialect = match (strtoupper($countryCode)) {
'SY' => 'السورية الشامية',
'JO' => 'الأردنية',
'EG' => 'المصرية العامية',
'IQ' => 'العراقية',
default => 'العربية الفصحى'
};
$savingsFormatted = number_format($savingsPct, 1);
$prompt = "
أنت كاتب إشعارات تسويقية ذكية لتطبيق Siro لخدمات نقل الركاب.
المستخدم الآن موجود بالقرب من '$zoneName'.
سعر سيرو أقل بـ $savingsFormatted% من $topCompetitor وبقية التطبيقات في هذه المنطقة.
المطلوب: اكتب إشعاراً push قصيراً جداً (عنوان + جسم) باللهجة $dialect.
- العنوان: لا يتجاوز 5 كلمات، مثير للاهتمام
- الجسم: لا يتجاوز 12 كلمة، يذكر التوفير الفعلي ويحفّز على الطلب الآن
- اجعله طبيعياً كأنه يكتبه شخص حقيقي وليس روبوت
الخرج (JSON فقط، بدون أي شرح):
{
\"push_title\": \"...\",
\"push_body\": \"...\"
}
";
return $this->callGemini($prompt, $model);
}
/**
* تقرأ معادلات المنافسين المكتشفة أسبوعياً وتقدم استراتيجية تسويق (Weekly Advisor).
*
* @param array $formulas المصفوفة المستخرجة من competitor_secret_formulas
* @param string $model
* @return array|null
*/
public function analyzeCompetitorFormulas(
array $formulas,
string $model = 'gemini-flash-lite-latest'
): ?array {
if (!$this->apiKey) return null;
$formulasJson = json_encode($formulas, JSON_UNESCAPED_UNICODE);
$prompt = "
أنت المستشار التسويقي المالي لتطبيق 'سيرو' لنقل الركاب.
لقد قمنا بعمل هندسة عكسية لرحلات منافسينا واكتشفنا معادلات التسعير السرية الخاصة بهم لهذا الأسبوع.
البيانات المكتشفة:
$formulasJson
المطلوب منك (كمستشار أعمال خبير):
1. تحليل هذه الأرقام، وتوضيح أين تكمن نقاط قوة المنافسين وأين نقاط ضعفهم في التسعير (مثلاً من يركز على المسافات القصيرة برفع فتح العداد؟).
2. اقتراح 3 استراتيجيات تسويقية أو رسائل إعلانية (Push Notifications/Social Media) موجهة للركاب، تستغل نقاط ضعف المنافسين المكتشفة.
قم بصياغة تقريرك بتنسيق HTML مرتب وجاهز للعرض في لوحة الإدارة (بدون علامات ```html)، واستخدم ألوان خفيفة في العناوين <h3>.
تحدث بلغة عربية احترافية ومباشرة لصناع القرار.
";
// بما أن الإخراج سيكون نص HTML، سنستخدم API جيميناي العادي بدون تقييد JSON
$url = $this->baseUrl . "{$model}:generateContent?key={$this->apiKey}";
$postData = [
'contents' => [
[
'parts' => [
['text' => $prompt]
]
]
],
'generationConfig' => [
'temperature' => 0.7,
'maxOutputTokens' => 2000
]
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
return ['status' => 'success', 'html_report' => $text];
} else {
error_log("[SiroGeminiService] HTTP $httpCode in analyzeCompetitorFormulas: $response");
return null;
}
}
/**
* Helper: يُرسل prompt لـ Gemini ويُعيد JSON مُفكَّك
*/
public function callGemini(string $prompt, string $model): ?array {
$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;
}
}
public function evaluatePostsForReporting(array $posts, string $model = 'gemini-flash-lite-latest'): ?string {
if (!$this->apiKey || empty($posts)) return null;
$postsJson = json_encode($posts, JSON_UNESCAPED_UNICODE);
$prompt = "
أنت محلل بيانات استخباراتية للسوق لتطبيق 'سيرو' لنقل الركاب.
إليك مجموعة من المنشورات والتعليقات التي جمعها الروبوت الخاص بنا من مجموعات فيسبوك اليوم:
$postsJson
المطلوب منك:
1. قراءة جميع هذه المنشورات واستخراج أي شكاوى، أسئلة، أو نقاشات تتعلق بـ (تطبيقات النقل الذكي، أسعار المحروقات، باقات الإنترنت، مشاكل السيارات).
ملاحظة هامة عن بنية البيانات:
- أي نص يبدأ بـ [FEED]: هو عبارة عن المنشور الأصلي (البوست).
- أي نص يبدأ بـ [COMMENT]: هو تعليق تابع للمنشور الذي يسبقه مباشرة.
2. تلخيص أهم هذه النقاشات في تقرير قصير ومفيد (News Report). يرجى التمييز بين المنشور الأصلي والتعليقات عليه لفهم السياق.
3. تجاهل المنشورات العشوائية أو الشخصية التي لا تفيد السوق.
هام جداً:
- يجب أن يكون التقرير باللغة العربية بالكامل.
- يجب أن تغلف التقرير بالكامل بوسم <div dir=\"rtl\" align=\"right\"> في البداية و </div> في النهاية لضمان القراءة من اليمين لليسار.
- استخدم <h3> للعناوين و <ul> للقوائم داخل التقرير.
- لا تقم بتضمين علامات ```html في المخرجات.
إذا لم يكن هناك أي شيء مفيد، اكتب فقط: <div dir=\"rtl\" align=\"right\"><p>لا توجد بيانات مفيدة في هذه الدفعة.</p></div>
";
$url = $this->baseUrl . "{$model}:generateContent?key={$this->apiKey}";
$postData = [
'contents' => [
[
'parts' => [
['text' => $prompt]
]
]
],
'generationConfig' => [
'temperature' => 0.5,
'maxOutputTokens' => 1500
]
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
return trim(preg_replace('/```html|```/', '', $text));
}
return null;
}
public function evaluateTelegramForReporting(array $messages, string $model = 'gemini-flash-lite-latest'): ?string {
if (!$this->apiKey || empty($messages)) return null;
$messagesJson = json_encode($messages, JSON_UNESCAPED_UNICODE);
$prompt = "
أنت محلل بيانات استخباراتية للسوق لتطبيق 'سيرو' لنقل الركاب.
إليك مجموعة من الرسائل التي جمعها الروبوت الخاص بنا من قنوات ومجموعات السائقين على تليغرام اليوم:
$messagesJson
المطلوب منك:
1. قراءة جميع هذه الرسائل واستخراج أي شكاوى، أسئلة، أو نقاشات تتعلق بـ (تطبيقات النقل الذكي مثل كريم، أوبر، جيني، يلاغو، تكسي إف، أسعار المحروقات، مشاكل التطبيقات، الإضرابات).
ملاحظة هامة عن بنية البيانات:
- كل رسالة تبدأ بـ [TELEGRAM]: هي عبارة عن رسالة مرسلة في مجموعة تليغرام.
2. تلخيص أهم هذه النقاشات في تقرير قصير ومفيد (News Report).
3. تجاهل رسائل الانضمام للمجموعات، الملصقات، الإعلانات العشوائية، أو الرسائل القصيرة جداً التي لا تحتوي على معنى مفيد للسوق.
هام جداً:
- يجب أن يكون التقرير باللغة العربية بالكامل.
- يجب أن تغلف التقرير بالكامل بوسم <div dir=\"rtl\" align=\"right\"> في البداية و </div> في النهاية لضمان القراءة من اليمين لليسار.
- استخدم <h3> للعناوين و <ul> للقوائم داخل التقرير.
- لا تقم بتضمين علامات ```html في المخرجات.
إذا لم يكن هناك أي شيء مفيد، اكتب فقط: <div dir=\"rtl\" align=\"right\"><p>لا توجد بيانات مفيدة في هذه الدفعة.</p></div>
";
$url = $this->baseUrl . "{$model}:generateContent?key={$this->apiKey}";
$postData = [
'contents' => [
[
'parts' => [
['text' => $prompt]
]
]
],
'generationConfig' => [
'temperature' => 0.5,
'maxOutputTokens' => 1500
]
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
echo "\n[GEMINI API ERROR] HTTP Code: $httpCode\n";
echo "Response: $response\n\n";
return null;
}
$data = json_decode($response, true);
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
return trim(preg_replace('/```html|```/', '', $text));
return null;
}
}