5% مستمر عبر آخر 48 ساعة → sustained_change (تغيير حقيقي مقترح) * - غير هيك → stable * * فترة سكون: أي تغيير "حقيقي" مقترح يُحتجز إذا كان آخر تغيير حقيقي * (would_apply=1 من نوع sustained_change) بآخر 3 أيام. * * جدولة مقترحة: كل 3 ساعات، بالتوازي مع cron_ai_engine.php. * * ملاحظة: competitor_formula_history جدول جديد — بيضل فاضي/قليل * البيانات لأول أسبوع بعد النشر لحد ما يتراكم تاريخ كافي لحساب * وسيط 7 أيام موثوق. هذا متوقع وطبيعي، مو خطأ. */ require_once __DIR__ . '/../core/bootstrap.php'; require_once __DIR__ . '/../functions.php'; try { $con = Database::get('main'); } catch (Exception $e) { die("Connection failed: " . $e->getMessage() . "\n"); } echo "Starting Pricing Stability Engine (Shadow Mode)...\n"; // ========================================== // 0. إنشاء الجداول إذا لم تكن موجودة // ========================================== $con->exec(" CREATE TABLE IF NOT EXISTS `pricing_stability_log` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `country_code` VARCHAR(5) NOT NULL, `tier` VARCHAR(20) NOT NULL DEFAULT 'economy', `reference_median_km_rate` DECIMAL(10,4) NOT NULL, `current_km_rate` DECIMAL(10,4) NOT NULL, `drift_pct` DECIMAL(6,2) NOT NULL, `classification` ENUM('stable','temporary_promo','sustained_change') NOT NULL, `suggested_action` TEXT NULL, `would_apply` TINYINT(1) NOT NULL DEFAULT 0, `hold_reason` VARCHAR(100) NULL, `evaluated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_country_time` (`country_code`, `evaluated_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); $con->exec(" CREATE TABLE IF NOT EXISTS `country_pricing_floor` ( `country_code` VARCHAR(5) NOT NULL, `min_km_rate` DECIMAL(10,4) NOT NULL, `min_base_fare` DECIMAL(10,4) NOT NULL DEFAULT 0, `updated_by` VARCHAR(100) NULL, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`country_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); // ========================================== // إعدادات // ========================================== $countryNameMap = ['JO' => 'Jordan', 'SY' => 'Syria', 'EG' => 'Egypt', 'IQ' => 'Iraq']; $tierPriority = ['economy', 'standard', 'premium']; $promoDropPct = -15.0; // انخفاض مفاجئ أكبر من هذا = برومو مؤقت $driftThreshold = 5.0; // انحراف أكبر من هذا (مطلق) يستاهل الانتباه $sustainedWindow = 48; // ساعة — لازم الانحراف يستمر عبرها $holdDays = 3; // فترة سكون بعد أي تغيير حقيقي مقترح $maxStepPct = 3.0; // أقصى تغيير مسموح بالمرة الوحدة $discountFactor = 1 - 0.065; /** وسيط بسيط لمصفوفة أرقام */ function median_of(array $nums): ?float { $nums = array_values(array_filter($nums, fn($n) => $n !== null && $n > 0)); if (empty($nums)) return null; sort($nums); $count = count($nums); $mid = intdiv($count, 2); if ($count % 2 === 0) { return ($nums[$mid - 1] + $nums[$mid]) / 2; } return $nums[$mid]; } foreach ($countryNameMap as $cc => $countryName) { echo "\n[$cc] Evaluating...\n"; // ── 1. سعر Siro الحالي بجدول kazan (للأرضية وحساب التغيير المقترح) ── $stmtKazan = $con->prepare("SELECT speedPrice, startPrice FROM kazan WHERE country = :country LIMIT 1"); $stmtKazan->execute([':country' => $countryName]); $kazanRow = $stmtKazan->fetch(PDO::FETCH_ASSOC); $currentSiroKmRate = $kazanRow ? (float)$kazanRow['speedPrice'] : 0.0; if ($currentSiroKmRate <= 0) { echo " ⚠️ No kazan row for $countryName. Skipping.\n"; continue; } // ── 2. أرضية ربحية — زرعها تلقائياً أول مرة بـ85% من السعر الحالي ── $stmtFloor = $con->prepare("SELECT min_km_rate FROM country_pricing_floor WHERE country_code = :cc"); $stmtFloor->execute([':cc' => $cc]); $floorRow = $stmtFloor->fetch(PDO::FETCH_ASSOC); if (!$floorRow) { $seedFloor = round($currentSiroKmRate * 0.85, 4); $insFloor = $con->prepare(" INSERT INTO country_pricing_floor (country_code, min_km_rate, min_base_fare, updated_by) VALUES (:cc, :floor, 0, 'auto_seed_default') "); $insFloor->execute([':cc' => $cc, ':floor' => $seedFloor]); $floorKmRate = $seedFloor; echo " 🌱 Seeded default profit floor: $seedFloor (85% of current price)\n"; } else { $floorKmRate = (float)$floorRow['min_km_rate']; } // ── 3. اختيار أفضل فئة متوفرة (economy أولاً) من المعادلات الحالية ── $currentTier = null; $currentKmRate = null; foreach ($tierPriority as $tier) { $stmtCur = $con->prepare(" SELECT price_per_km FROM competitor_secret_formulas WHERE country_code = :cc AND tier = :tier ORDER BY last_updated DESC LIMIT 1 "); $stmtCur->execute([':cc' => $cc, ':tier' => $tier]); $row = $stmtCur->fetch(PDO::FETCH_ASSOC); if ($row && (float)$row['price_per_km'] > 0) { $currentTier = $tier; $currentKmRate = (float)$row['price_per_km']; break; } } if ($currentKmRate === null) { echo " ⚠️ No current competitor formula for $countryName. Skipping.\n"; continue; } // ── 4. الوسيط المرجعي لآخر 7 أيام من سجل التاريخ ── $stmtHist = $con->prepare(" SELECT price_per_km FROM competitor_formula_history WHERE country_code = :cc AND tier = :tier AND snapshotted_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY snapshotted_at ASC "); $stmtHist->execute([':cc' => $cc, ':tier' => $currentTier]); $historyRates = array_map('floatval', $stmtHist->fetchAll(PDO::FETCH_COLUMN)); $referenceMedian = median_of($historyRates); if ($referenceMedian === null) { echo " ℹ️ Not enough history yet for $countryName [tier=$currentTier] — building up (need ~7 days). Skipping evaluation.\n"; continue; } // ── 5. حساب الانحراف ── $driftPct = round((($currentKmRate - $referenceMedian) / $referenceMedian) * 100, 2); // ── 6. هل الانحراف مستمر آخر 48 ساعة؟ ── $stmtRecent = $con->prepare(" SELECT price_per_km FROM competitor_formula_history WHERE country_code = :cc AND tier = :tier AND snapshotted_at >= DATE_SUB(NOW(), INTERVAL :hours HOUR) ORDER BY snapshotted_at ASC "); $stmtRecent->execute([':cc' => $cc, ':tier' => $currentTier, ':hours' => $sustainedWindow]); $recentRates = array_map('floatval', $stmtRecent->fetchAll(PDO::FETCH_COLUMN)); $isSustained = false; if (count($recentRates) >= 2) { $sameDirectionCount = 0; foreach ($recentRates as $r) { $rDrift = (($r - $referenceMedian) / $referenceMedian) * 100; $sameSign = ($driftPct >= 0 && $rDrift >= 0) || ($driftPct < 0 && $rDrift < 0); if ($sameSign && abs($rDrift) > $driftThreshold) { $sameDirectionCount++; } } // كل اللقطات بآخر 48 ساعة (وليس عينة واحدة) تؤكد نفس الاتجاه $isSustained = ($sameDirectionCount === count($recentRates)); } // ── 7. التصنيف ── if ($driftPct <= $promoDropPct) { $classification = 'temporary_promo'; } elseif (abs($driftPct) > $driftThreshold && $isSustained) { $classification = 'sustained_change'; } else { $classification = 'stable'; } // ── 8. الإجراء المقترح + would_apply + فترة السكون ── $wouldApply = false; $holdReason = null; $suggestedAction = 'لا إجراء — السعر ثابت'; if ($classification === 'temporary_promo') { $wouldApply = true; $suggestedAction = "منافس نازل مؤقتاً بنسبة " . abs($driftPct) . "% تحت المرجع — الرد المقترح: كود خصم مؤقت عبر محرك التسويق، بدون لمس السعر الأساسي"; } elseif ($classification === 'sustained_change') { // تحقق من فترة السكون بالاعتماد على سجلّنا الخاص فقط (shadow-only) $stmtLastApply = $con->prepare(" SELECT evaluated_at FROM pricing_stability_log WHERE country_code = :cc AND classification = 'sustained_change' AND would_apply = 1 ORDER BY evaluated_at DESC LIMIT 1 "); $stmtLastApply->execute([':cc' => $cc]); $lastApply = $stmtLastApply->fetchColumn(); $inHoldPeriod = false; if ($lastApply) { $daysSince = (time() - strtotime($lastApply)) / 86400; $inHoldPeriod = $daysSince < $holdDays; } // السعر المستهدف: نفس منطق الخصم 6.5% المعتمد، بحد أقصى 3% تغيير بالمرة، ومقيّد بالأرضية $rawTarget = $currentKmRate * $discountFactor; $maxUp = $currentSiroKmRate * (1 + $maxStepPct / 100); $maxDown = $currentSiroKmRate * (1 - $maxStepPct / 100); $clampedTarget = max($maxDown, min($maxUp, $rawTarget)); $clampedTarget = max($clampedTarget, $floorKmRate); $clampedTarget = round($clampedTarget, 4); if ($inHoldPeriod) { $wouldApply = false; $holdReason = 'hold_period_active'; $suggestedAction = "تغيير مستمر ({$driftPct}%) — بس بفترة سكون (آخر تغيير قبل أقل من $holdDays أيام). لو مفعّل: من $currentSiroKmRate إلى $clampedTarget"; } else { $wouldApply = true; $suggestedAction = "تغيير مستمر ({$driftPct}%) — سعر كيلو مقترح: من $currentSiroKmRate إلى $clampedTarget (مقيّد بـ{$maxStepPct}% والأرضية $floorKmRate)"; } } // ── 9. تسجيل النتيجة ── $insLog = $con->prepare(" INSERT INTO pricing_stability_log (country_code, tier, reference_median_km_rate, current_km_rate, drift_pct, classification, suggested_action, would_apply, hold_reason) VALUES (:cc, :tier, :ref, :cur, :drift, :cls, :action, :apply, :hold) "); $insLog->execute([ ':cc' => $cc, ':tier' => $currentTier, ':ref' => $referenceMedian, ':cur' => $currentKmRate, ':drift' => $driftPct, ':cls' => $classification, ':action' => $suggestedAction, ':apply' => $wouldApply ? 1 : 0, ':hold' => $holdReason, ]); $icon = $classification === 'stable' ? '✅' : ($classification === 'temporary_promo' ? '🎯' : '⚠️'); echo " $icon [tier=$currentTier] drift={$driftPct}% → $classification\n"; echo " $suggestedAction\n"; } echo "\nPricing Stability Engine finished (shadow mode — kazan not touched).\n";