Files
tripz-llc/backend/bot/cron_passenger_reengagement.php
2026-08-09 16:56:13 +03:00

187 lines
7.6 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");
}
// getRedisConnection() لا ترمي استثناءً — تُعيد null عند تعذّر الاتصال.
// مفتاح مكافحة التكرار يعيش في Redis؛ بدونه يفشل الفحص "مفتوحاً"
// فيتلقّى كل راكب نفس رسالة الاستعادة في كل تشغيل.
if (!$redis) {
error_log('[ReEngagement] ABORT: Redis unavailable — anti-spam guard would fail open.');
die("[ReEngagement] Redis unavailable — aborting to avoid duplicate pushes.\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";
?>