From 3c9a3dbef8bc484048b0d4c453d8c462bc2da4ef Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Mon, 6 Jul 2026 19:17:20 +0300 Subject: [PATCH] Update: 2026-07-06 19:17:19 --- backend/bot/cron_ai_engine.php | 35 +++++++++++++++++- .../migrations/003_add_surge_zones.sql | 16 ++++++++ backend/pricing-engine/src/db/connection.ts | 37 +++++++++++++++++++ backend/pricing-engine/src/index.ts | 27 +++++++++++++- 4 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 backend/pricing-engine/migrations/003_add_surge_zones.sql diff --git a/backend/bot/cron_ai_engine.php b/backend/bot/cron_ai_engine.php index f36847fa..9b0fe7b6 100644 --- a/backend/bot/cron_ai_engine.php +++ b/backend/bot/cron_ai_engine.php @@ -51,7 +51,7 @@ try { // 1. Economy tier (الأكثر تنافسية) // 2. Standard tier (إذا ما في Economy) // 3. أي tier ثاني بأعلى R² - $tierPriority = ['economy', 'standard', 'premium']; + $tierPriority = ['standard', 'economy', 'premium']; $bestFormula = null; $bestTierIdx = 999; @@ -131,7 +131,8 @@ try { awfarPrice = :awfarPrice, normalMinPrice = :normalMin, peakMinPrice = :peakMin, - lateMinPrice = :lateMin + lateMinPrice = :lateMin, + startPrice = :startPrice WHERE country = :countryName"; $upStmt = $con->prepare($updateSql); @@ -148,6 +149,7 @@ try { ':normalMin' => $normalMin, ':peakMin' => $peakMin, ':lateMin' => $lateMin, + ':startPrice' => $ourBase, ':countryName' => $countryName ]); @@ -230,4 +232,33 @@ try { echo " ❌ Error in Smart Retention: " . $e->getMessage() . "\n"; } +// ========================================== +// 4. Export AI Hotzones to Redis for Captains +// ========================================== +echo "4. Exporting AI Hotzones to Redis...\n"; +try { + $sql = "SELECT latitude, longitude, competitor_name AS top_competitor, avg_ppk AS avg_price + FROM competitor_surge_zones + WHERE detected_at >= DATE_SUB(NOW(), INTERVAL 6 HOUR) + AND surge_multiplier > 1.1"; + + $stmt = $con->query($sql); + $zones = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (!empty($zones)) { + $redisData = [ + 'status' => 'success', + 'data' => $zones + ]; + $redis->setex('siro:cache:ai:hotzones', 3600, json_encode($redisData)); + echo " 🗺️ Exported " . count($zones) . " hot zones to Redis (siro:cache:ai:hotzones).\n"; + } else { + // Clear if no surge to avoid stale data + $redis->del('siro:cache:ai:hotzones'); + echo " ℹ️ No active hot zones to export.\n"; + } +} catch (Exception $e) { + echo " ❌ Error in Hotzones Export: " . $e->getMessage() . "\n"; +} + echo "AI Engine v2 finished successfully.\n"; diff --git a/backend/pricing-engine/migrations/003_add_surge_zones.sql b/backend/pricing-engine/migrations/003_add_surge_zones.sql new file mode 100644 index 00000000..c37c6cea --- /dev/null +++ b/backend/pricing-engine/migrations/003_add_surge_zones.sql @@ -0,0 +1,16 @@ +-- Migration: Create competitor_surge_zones table for heatmap integration +-- Purpose: Bridge the geographic surge anomalies from Node.js pricing engine to PHP heatmap + +CREATE TABLE IF NOT EXISTS `competitor_surge_zones` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `competitor_name` VARCHAR(100) NOT NULL, + `country_code` VARCHAR(5) NOT NULL, + `zone_key` VARCHAR(50) NOT NULL, + `latitude` DECIMAL(10,6) NOT NULL, + `longitude` DECIMAL(10,6) NOT NULL, + `avg_ppk` DECIMAL(10,3) NOT NULL, + `sample_count` INT NOT NULL, + `surge_multiplier` DECIMAL(5,3) NOT NULL DEFAULT 1.000, + `detected_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY `unique_zone` (`competitor_name`, `country_code`, `zone_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/backend/pricing-engine/src/db/connection.ts b/backend/pricing-engine/src/db/connection.ts index 3e8771e9..b72490e3 100644 --- a/backend/pricing-engine/src/db/connection.ts +++ b/backend/pricing-engine/src/db/connection.ts @@ -164,6 +164,43 @@ export async function saveSurgeInsights( await pool.execute(sql, flatParams); } +export async function saveSurgeZones( + pool: mysql.Pool, + zones: Array<{ + competitorName: string; + countryCode: string; + zoneKey: string; + latitude: number; + longitude: number; + avgPpk: number; + sampleCount: number; + surgeMultiplier: number; + }> +): Promise { + if (zones.length === 0) return; + + const values = zones.map(() => `(?, ?, ?, ?, ?, ?, ?, ?, NOW())`).join(','); + const flatParams: (string | number)[] = []; + + for (const z of zones) { + flatParams.push( + z.competitorName, z.countryCode, z.zoneKey, + z.latitude, z.longitude, z.avgPpk, z.sampleCount, z.surgeMultiplier + ); + } + + const sql = `INSERT INTO competitor_surge_zones + (competitor_name, country_code, zone_key, latitude, longitude, avg_ppk, sample_count, surge_multiplier, detected_at) + VALUES ${values} + ON DUPLICATE KEY UPDATE + avg_ppk = VALUES(avg_ppk), + sample_count = VALUES(sample_count), + surge_multiplier = VALUES(surge_multiplier), + detected_at = NOW()`; + + await pool.execute(sql, flatParams); +} + export async function closeConnections(): Promise { if (mysqlPool) { await mysqlPool.end(); diff --git a/backend/pricing-engine/src/index.ts b/backend/pricing-engine/src/index.ts index 9927741f..7820564e 100644 --- a/backend/pricing-engine/src/index.ts +++ b/backend/pricing-engine/src/index.ts @@ -10,7 +10,7 @@ * Cron integration: see crontab examples in package.json scripts */ -import { getMySQL, fetchSamples, saveFormulas, saveSurgeInsights, closeConnections } from './db/connection'; +import { getMySQL, fetchSamples, saveFormulas, saveSurgeInsights, saveSurgeZones, closeConnections } from './db/connection'; import { runAnalysis } from './analysis/engine'; import { Pool, RowDataPacket } from 'mysql2/promise'; @@ -255,6 +255,31 @@ async function processCompetitor( await saveSurgeInsights(pool, surgeInsights); console.log(` ✅ Saved surge insight: avg ${avgMultiplier.toFixed(3)}x, hours ${peakStart}:00-${peakEnd}:00`); } + + // --- Save surge zones --- + if (opts.mode !== 'report' && report.zones.length > 0) { + // Find the standard tier formula to use as a baseline for calculating surge multipliers + const standardTier = formulas.find(f => f.tier === 'standard') || formulas[0]; + const baselinePpk = standardTier ? standardTier.kmRate : 0.350; + + const surgeZones = report.zones + .filter(z => z.avgPpk > baselinePpk * 1.1) // Only keep zones with > 10% surge + .map(z => ({ + competitorName: comp.competitor_name, + countryCode: comp.country_code, + zoneKey: z.zoneKey, + latitude: z.centerLat, + longitude: z.centerLng, + avgPpk: z.avgPpk, + sampleCount: z.samples.length, + surgeMultiplier: parseFloat((z.avgPpk / baselinePpk).toFixed(3)), + })); + + if (surgeZones.length > 0) { + await saveSurgeZones(pool, surgeZones); + console.log(` ✅ Saved ${surgeZones.length} surge zones for heatmap`); + } + } } async function fetchCompetitors(