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

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

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

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

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

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

179 lines
7.1 KiB
PHP

<?php
/**
* cron_passenger_reengagement.php
* ─────────────────────────────────────────────────────────────
* Re-engagement cron: يرسل إشعار تسويقي مخصص (عبر Gemini AI)
* للركاب الذين لم يفتحوا التطبيق منذ 3 أيام أو أكثر.
*
* المصدر: جدول passenger_opening_locations (آخر نشاط مسجّل)
* Anti-spam: Redis — لا يُرسَل لنفس الراكب أكثر من مرة كل 72 ساعة
*
* جدولة مقترحة (crontab):
* 0 10 * * * php /var/www/backend/bot/cron_passenger_reengagement.php
* (يعمل يومياً الساعة 10 صباحاً)
*/
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../core/Services/SiroGeminiService.php';
require_once __DIR__ . '/../core/Security/EncryptionHelper.php';
require_once __DIR__ . '/../functions.php';
set_time_limit(0);
ini_set('memory_limit', '256M');
try {
$con = Database::get('main');
$redis = getRedisConnection();
} catch (Exception $e) {
die("[ReEngagement] Connection failed: " . $e->getMessage() . "\n");
}
echo "[ReEngagement] Starting Passenger Re-engagement Engine...\n";
$REDIS_TTL = 72 * 3600; // anti-spam: 72 ساعة
$INACTIVE_DAYS = 3; // عدد أيام الخمول
$BATCH_LIMIT = 500; // أقصى عدد ركاب لكل تشغيل
// ─────────────────────────────────────────────────────────────
// 1. جلب الركاب الخاملين (آخر نشاطهم منذ 3 أيام أو أكثر)
// نعتمد على passenger_opening_locations كمصدر لآخر نشاط
// ─────────────────────────────────────────────────────────────
$sql = "
SELECT
p.id AS passenger_id,
IFNULL(MAX(pol.country_code), 'JO') AS country_code,
t.token,
MAX(pol.created_at) AS last_active
FROM passengers p
JOIN tokens t ON t.passengerID = p.id
LEFT JOIN passenger_opening_locations pol ON pol.passenger_id = p.id
WHERE p.status = 'notDeleted'
GROUP BY p.id, t.token
HAVING
last_active IS NULL
OR last_active < DATE_SUB(NOW(), INTERVAL :days DAY)
ORDER BY last_active ASC
LIMIT :limit
";
$stmt = $con->prepare($sql);
$stmt->bindValue(':days', $INACTIVE_DAYS, PDO::PARAM_INT);
$stmt->bindValue(':limit', $BATCH_LIMIT, PDO::PARAM_INT);
$stmt->execute();
$inactivePassengers = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($inactivePassengers)) {
echo "[ReEngagement] No inactive passengers found. All good!\n";
exit;
}
echo "[ReEngagement] Found " . count($inactivePassengers) . " inactive passengers (≥{$INACTIVE_DAYS} days).\n";
// ─────────────────────────────────────────────────────────────
// 2. تجميع حسب الدولة لتقليل استدعاءات Gemini
// ─────────────────────────────────────────────────────────────
$byCountry = [];
foreach ($inactivePassengers as $p) {
$cc = strtoupper($p['country_code'] ?? 'SY');
$byCountry[$cc][] = $p;
}
$geminiService = new SiroGeminiService();
$encryptionHelper = new EncryptionHelper();
$sentCount = 0;
$skippedCount = 0;
foreach ($byCountry as $countryCode => $passengers) {
echo "[ReEngagement] Country: $countryCode — " . count($passengers) . " passengers.\n";
// ─── رسالة Gemini مخصصة لدولة واحدة ───────────────────
$regionName = match($countryCode) {
'JO' => 'Amman',
'SY' => 'Damascus',
'EG' => 'Cairo',
'IQ' => 'Baghdad',
default => 'your city'
};
// سحب أسعار المنافسين كمرجع للرسالة
$stmtComp = $con->prepare(
"SELECT competitor_name, base_fare FROM competitor_secret_formulas
WHERE country_code = :cc ORDER BY last_updated DESC LIMIT 3"
);
$stmtComp->execute([':cc' => $countryCode]);
$competitors = $stmtComp->fetchAll(PDO::FETCH_ASSOC);
if (empty($competitors)) {
$competitors = [['competitor_name' => 'Market Average', 'base_fare' => 1.0]];
}
$aiCampaign = $geminiService->analyzeMarketAndDraftCampaign(
$competitors,
1.0,
$regionName,
$countryCode
);
// Fallback إذا Gemini ما أعطى فرصة
if (!$aiCampaign || ($aiCampaign['opportunity_detected'] ?? false) !== true) {
$countryEmoji = match($countryCode) { 'JO' => '🇯🇴', 'SY' => '🇸🇾', 'EG' => '🇪🇬', default => '🌍' };
$title = "وينك؟ نفتقدك! {$countryEmoji}";
$body = "مش شايفينك من {$INACTIVE_DAYS} أيام. سيارتك جاهزة وبأرخص سعر — اطلب الآن! 🚗";
} else {
$title = $aiCampaign['push_title'] ?? '🚗 سيرو يفتقدك!';
$body = $aiCampaign['push_body'] ?? "لم تطلب منذ {$INACTIVE_DAYS} أيام — العروض لا تنتظر!";
}
echo "[ReEngagement] Message: [{$title}] {$body}\n";
// ─── إرسال لكل راكب مع anti-spam ───────────────────────
foreach ($passengers as $p) {
$passengerId = $p['passenger_id'];
$redisKey = "reengagement:sent:{$passengerId}";
// Anti-spam: تخطّى إذا أرسلنا له خلال 72 ساعة
if ($redis && $redis->exists($redisKey)) {
$skippedCount++;
continue;
}
$decryptedToken = $encryptionHelper->decryptData($p['token'] ?? '');
if (!$decryptedToken) continue;
$result = sendFCM_Internal(
$decryptedToken,
$title,
$body,
[
'type' => 're_engagement',
'days_inactive' => $INACTIVE_DAYS,
'screen' => 'home',
],
'Marketing',
false,
'ding'
);
// حفظ في Redis لمنع التكرار
if ($redis) {
$redis->setex($redisKey, $REDIS_TTL, '1');
}
// حفظ في جدول notifications الداخلي (يظهر في صفحة الإشعارات)
try {
$ins = $con->prepare(
"INSERT INTO notifications (title, body, passenger_id)
VALUES (:title, :body, :pid)"
);
$ins->execute([':title' => $title, ':body' => $body, ':pid' => $passengerId]);
} catch (Exception $e) {
error_log("[ReEngagement] DB insert error: " . $e->getMessage());
}
$sentCount++;
}
}
echo "[ReEngagement] Done ✅ — Sent: {$sentCount}, Skipped (anti-spam): {$skippedCount}\n";
?>