187 lines
7.6 KiB
PHP
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";
|
|
?>
|