Update: 2026-07-09 01:01:15
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
// ============================================================
|
||||||
|
// get_pricing_stability_log.php
|
||||||
|
// شاشة مراجعة محرك الثبات (Shadow Mode) — يعرض سجل التصنيفات
|
||||||
|
// والإجراءات المقترحة بدون ما يكون أي منها مطبّق فعلياً على kazan
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../connect.php';
|
||||||
|
|
||||||
|
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$countryCode = filterRequest('country_code');
|
||||||
|
$limit = filterRequest('limit', 'int') ?? 100;
|
||||||
|
|
||||||
|
$sql = "SELECT * FROM pricing_stability_log";
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
if ($countryCode) {
|
||||||
|
$sql .= " WHERE country_code = :country";
|
||||||
|
$params[':country'] = strtoupper($countryCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY evaluated_at DESC LIMIT :limit";
|
||||||
|
|
||||||
|
$stmt = $con->prepare($sql);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
foreach ($params as $key => $val) {
|
||||||
|
$stmt->bindValue($key, $val);
|
||||||
|
}
|
||||||
|
$stmt->execute();
|
||||||
|
$log = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// ملخص سريع لآخر تصنيف لكل دولة
|
||||||
|
$stmtLatest = $con->query("
|
||||||
|
SELECT l1.* FROM pricing_stability_log l1
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT country_code, MAX(evaluated_at) AS max_time
|
||||||
|
FROM pricing_stability_log
|
||||||
|
GROUP BY country_code
|
||||||
|
) l2 ON l1.country_code = l2.country_code AND l1.evaluated_at = l2.max_time
|
||||||
|
");
|
||||||
|
$latestPerCountry = $stmtLatest->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
jsonSuccess([
|
||||||
|
'log' => $log,
|
||||||
|
'latest_per_country' => $latestPerCountry,
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("[get_pricing_stability_log.php] Error: " . $e->getMessage());
|
||||||
|
jsonError("Failed to fetch pricing stability log: " . $e->getMessage());
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* cron_pricing_stability_engine.php
|
||||||
|
* ─────────────────────────────────────────────────────────────
|
||||||
|
* محرك الثبات والاستقرار — وضع مراقبة (Shadow Mode) فقط.
|
||||||
|
*
|
||||||
|
* ما بيلمس جدول kazan أبداً. بيقارن معامل سعر الكيلو الحالي لكل
|
||||||
|
* منافس/دولة (من competitor_secret_formulas) مع الوسيط المرجعي
|
||||||
|
* لآخر 7 أيام (من competitor_formula_history)، ويسجّل تصنيفه
|
||||||
|
* والإجراء المقترح بجدول pricing_stability_log لمراجعته يدوياً.
|
||||||
|
*
|
||||||
|
* التصنيف:
|
||||||
|
* - drift <= -15% → temporary_promo (برومو مؤقت — لا يُغيَّر السعر الأساسي)
|
||||||
|
* - |drift| > 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";
|
||||||
@@ -129,6 +129,46 @@ export async function saveFormulas(
|
|||||||
await pool.execute(sql, flatParams);
|
await pool.execute(sql, flatParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* لقطة insert-only من معاملات كل معادلة — لا تُستبدل أبداً، بعكس
|
||||||
|
* competitor_secret_formulas (UPSERT). هاي المصدر اللي يقارن عليه
|
||||||
|
* محرك الثبات (cron_pricing_stability_engine.php) "المعامل اليوم
|
||||||
|
* مقابل المعامل قبل أسبوع" لتمييز التغيير الحقيقي عن البرومو المؤقت.
|
||||||
|
*/
|
||||||
|
export async function saveFormulaHistory(
|
||||||
|
pool: mysql.Pool,
|
||||||
|
formulas: Array<{
|
||||||
|
competitorName: string;
|
||||||
|
countryCode: string;
|
||||||
|
tier: string;
|
||||||
|
baseFare: number;
|
||||||
|
kmRate: number;
|
||||||
|
minRate: number;
|
||||||
|
minFare: number;
|
||||||
|
rSquared: number;
|
||||||
|
sampleCount: number;
|
||||||
|
}>
|
||||||
|
): Promise<void> {
|
||||||
|
if (formulas.length === 0) return;
|
||||||
|
|
||||||
|
const values = formulas.map(() => `(?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`).join(',');
|
||||||
|
const flatParams: (string | number)[] = [];
|
||||||
|
|
||||||
|
for (const f of formulas) {
|
||||||
|
flatParams.push(
|
||||||
|
f.competitorName, f.countryCode, f.tier,
|
||||||
|
f.baseFare, f.kmRate, f.minRate, f.minFare,
|
||||||
|
f.rSquared, f.sampleCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `INSERT INTO competitor_formula_history
|
||||||
|
(competitor_name, country_code, tier, base_fare, price_per_km, price_per_min, min_fare, r_squared, sample_size, snapshotted_at)
|
||||||
|
VALUES ${values}`;
|
||||||
|
|
||||||
|
await pool.execute(sql, flatParams);
|
||||||
|
}
|
||||||
|
|
||||||
export async function saveSurgeInsights(
|
export async function saveSurgeInsights(
|
||||||
pool: mysql.Pool,
|
pool: mysql.Pool,
|
||||||
insights: Array<{
|
insights: Array<{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* Cron integration: see crontab examples in package.json scripts
|
* Cron integration: see crontab examples in package.json scripts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getMySQL, fetchSamples, saveFormulas, saveSurgeInsights, saveSurgeZones, closeConnections } from './db/connection';
|
import { getMySQL, fetchSamples, saveFormulas, saveFormulaHistory, saveSurgeInsights, saveSurgeZones, closeConnections } from './db/connection';
|
||||||
import { runAnalysis } from './analysis/engine';
|
import { runAnalysis } from './analysis/engine';
|
||||||
import { Pool, RowDataPacket } from 'mysql2/promise';
|
import { Pool, RowDataPacket } from 'mysql2/promise';
|
||||||
|
|
||||||
@@ -236,6 +236,19 @@ async function processCompetitor(
|
|||||||
if (peakHours.length > 0) {
|
if (peakHours.length > 0) {
|
||||||
console.log(` Peak hours stored: [${peakHours.join(', ')}]`);
|
console.log(` Peak hours stored: [${peakHours.join(', ')}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// لقطة insert-only لمحرك الثبات (drift detection) — لا تُستبدل أبداً
|
||||||
|
await saveFormulaHistory(pool, formulas.map(f => ({
|
||||||
|
competitorName: f.competitorName,
|
||||||
|
countryCode: f.countryCode,
|
||||||
|
tier: f.tier,
|
||||||
|
baseFare: f.baseFare,
|
||||||
|
kmRate: f.kmRate,
|
||||||
|
minRate: f.minRate,
|
||||||
|
minFare: f.minFare,
|
||||||
|
rSquared: f.rSquared,
|
||||||
|
sampleCount: f.sampleCount,
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Save surge insights ---
|
// --- Save surge insights ---
|
||||||
|
|||||||
Reference in New Issue
Block a user