435 lines
19 KiB
Markdown
435 lines
19 KiB
Markdown
# Siro Pricing Engine — Architecture & Deployment Guide
|
||
|
||
<div dir="rtl">
|
||
|
||
## 1. نظرة عامة على المنظومة
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ Siro PRICING ECOSYSTEM │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌──────────────────────┐ ┌──────────────────────────────┐ │
|
||
│ │ Android Bot Scraper │ │ Node.js Pricing Engine │ │
|
||
│ │ (Java/Kotlin) │─────▶│ (TypeScript) │ │
|
||
│ │ يرسخن أسعار │ │ تحليل إحصائي متقدم │ │
|
||
│ │ TaxiF, Careem, Uber │ │ ┌────────────────────────┐ │ │
|
||
│ │ Jeeny... │ │ │ MAD Outlier Detection │ │ │
|
||
│ └──────────┬───────────┘ │ │ K-Means Tier Clustering│ │ │
|
||
│ │ │ │ Ridge Regression (MLR) │ │ │
|
||
│ ▼ │ │ Min Fare Detection │ │ │
|
||
│ ┌──────────────────────┐ │ │ Surge Analysis │ │ │
|
||
│ │ MySQL: │ │ │ Zone Pricing │ │ │
|
||
│ │ scraped_competitor_ │◀─────│ └────────────────────────┘ │ │
|
||
│ │ prices │ └──────────────┬───────────────┘ │
|
||
│ └──────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ ▼ ▼ │
|
||
│ ┌──────────────────────┐ ┌──────────────────────────────┐ │
|
||
│ │ MySQL: │ │ MySQL: │ │
|
||
│ │ competitor_secret_ │ │ competitor_surge_insights │ │
|
||
│ │ formulas │ │ (ساعات الذروة + المضاعف) │ │
|
||
│ │ (معادلات المنافسين) │ └──────────────┬───────────────┘ │
|
||
│ └──────────┬───────────┘ │ │
|
||
│ │ │ │
|
||
│ ▼ ▼ │
|
||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||
│ │ PHP Cron Jobs (Backend) │ │
|
||
│ │ │ │
|
||
│ │ cron_ai_engine.php: يقرأ المعادلات ويحدث kazan │ │
|
||
│ │ cron_kazan_adjuster: يقرأ surge ويضبط العمولة │ │
|
||
│ │ cron_gemini_advisor: يرسل المعادلات لـ Gemini لتقارير │ │
|
||
│ └────────────────────────┬───────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||
│ │ Redis Cache Layer │ │
|
||
│ │ │ │
|
||
│ │ surge:opportunities → مضاعف Surge المقترح │ │
|
||
│ │ surge:opportunities:{JO} → لكل دولة │ │
|
||
│ │ siro:cache:pricing:grids → أسعار حسب Grid 2.5km │ │
|
||
│ └────────────────────────┬───────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||
│ │ Real-time APIs (Rider & Driver Apps) │ │
|
||
│ │ │ │
|
||
│ │ ride/pricing/get.php: حساب السعر الفوري للراكب │ │
|
||
│ │ ride/heatmap/: خريطة حرارية للسائق │ │
|
||
│ │ api/ride/competitor: مقارنة أسعار المنافسين │ │
|
||
│ └────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Deployment: Node.js إلى جانب PHP
|
||
|
||
### المشكلة
|
||
|
||
النظام الحالي PHP على Apache/Nginx. نحتاج Node.js للتشغيل جنباً إلى جنب.
|
||
|
||
### الحل: PM2 Process Manager
|
||
|
||
```bash
|
||
# 1. تثبيت Node.js على السيرفر (مرة واحدة)
|
||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||
sudo apt-get install -y nodejs
|
||
|
||
# 2. رفع مجلد pricing-engine إلى السيرفر
|
||
# (scp أو git pull)
|
||
|
||
# 3. تثبيت PM2 (مدير عمليات Node.js)
|
||
npm install -g pm2
|
||
|
||
# 4. تثبيت dependencies
|
||
cd /var/www/siro/backend/pricing-engine
|
||
npm install
|
||
cp .env.example .env
|
||
# عدّل .env ببيانات MySQL + Redis
|
||
|
||
# 5. تشغيل الخدمات مع PM2
|
||
pm2 start ecosystem.config.js
|
||
pm2 save
|
||
pm2 startup # عشان يشتغل تلقائياً بعد reboot
|
||
```
|
||
|
||
### ملف PM2 Ecosystem
|
||
|
||
```javascript
|
||
// backend/pricing-engine/ecosystem.config.js
|
||
module.exports = {
|
||
apps: [
|
||
{
|
||
name: 'siro-pricing-hourly',
|
||
script: 'dist/index.js',
|
||
args: '--mode=surge --hours=3',
|
||
cron_restart: '0 * * * *', // كل ساعة
|
||
autorestart: false,
|
||
time: true,
|
||
},
|
||
{
|
||
name: 'siro-pricing-daily',
|
||
script: 'dist/index.js',
|
||
args: '--mode=full --hours=72',
|
||
cron_restart: '0 6 * * *', // كل يوم 6 صباحاً
|
||
autorestart: false,
|
||
time: true,
|
||
},
|
||
{
|
||
name: 'siro-pricing-weekly',
|
||
script: 'dist/index.js',
|
||
args: '--mode=report --hours=168',
|
||
cron_restart: '0 8 * * 1', // كل أسبوع الإثنين 8 صباحاً
|
||
autorestart: false,
|
||
time: true,
|
||
},
|
||
],
|
||
};
|
||
```
|
||
|
||
> PM2 يتولى تشغيل cron jobs بدون الحاجة إلى `crontab` نظامي.
|
||
> لكنه `autorestart: false` لأنها عمليات لمرة واحدة، مو servers.
|
||
|
||
### بديل: Crontab عادي (أبسط)
|
||
|
||
```bash
|
||
# crontab -e
|
||
0 * * * * cd /var/www/siro/backend/pricing-engine && node dist/index.js --mode=surge --hours=3 >> /var/log/siro-pricing.log 2>&1
|
||
0 6 * * * cd /var/www/siro/backend/pricing-engine && node dist/index.js --mode=full --hours=72 >> /var/log/siro-pricing.log 2>&1
|
||
0 8 * * 1 cd /var/www/siro/backend/pricing-engine && node dist/index.js --mode=report --hours=168 >> /var/log/siro-pricing.log 2>&1
|
||
```
|
||
|
||
---
|
||
|
||
## 3. دفق التسعير الكامل (Full Pricing Flow)
|
||
|
||
### 3.1 تحليل المنافسين ← حفظ المعادلات
|
||
|
||
```
|
||
Pricing Engine (Node.js)
|
||
│
|
||
├─ 1. يسحب بيانات من scraped_competitor_prices
|
||
├─ 2. ينظف الشواذ (MAD)
|
||
├─ 3. يصنّف الفئات (K-Means) → Economy / Standard / Premium
|
||
├─ 4. يحسب الانحدار لكل فئة:
|
||
│ price = baseFare + kmRate×dist + minRate×duration
|
||
├─ 5. يكتشف Minimum Fare
|
||
├─ 6. يحلل Surge حسب الساعة
|
||
└─ 7. يحفظ في:
|
||
├─ competitor_secret_formulas (معادلات لكل tier)
|
||
└─ competitor_surge_insights (ساعات الذروة)
|
||
```
|
||
|
||
### 3.2 PHP يقرأ ويطبّق التسعير
|
||
|
||
```
|
||
cron_ai_engine.php (PHP, كل 30-60 دقيقة)
|
||
│
|
||
├─ 1. يقرأ competitor_secret_formulas
|
||
├─ 2. يختار Economy tier (الأرخص)
|
||
├─ 3. يطبّق خصم 6.5%:
|
||
│ Siro_kmRate = competitor_kmRate × 0.935
|
||
├─ 4. يحدّث جدول kazan
|
||
└─ 5. يحفظ surge في Redis
|
||
```
|
||
|
||
### 3.3 حساب السعر للتطبيقات
|
||
|
||
```
|
||
ride/pricing/get.php (API, يتم استدعاؤه عند طلب رحلة)
|
||
│
|
||
├─ 1. يقرأ kazan table (آخر تحديث من cron_ai_engine)
|
||
├─ 2. يحسب:
|
||
│ basePrice = kazan.baseFare
|
||
│ + kazan.speedPrice × distance
|
||
│ + kazan.normalMinPrice × duration
|
||
├─ 3. يقرأ Redis surge:opportunities
|
||
├─ 4. يطبّق surge multiplier إذا كانت ساعة ذروة
|
||
└─ 5. يرجع السعر النهائي للتطبيق
|
||
```
|
||
|
||
---
|
||
|
||
## 4. تقسيم المناطق (Zone-Based Pricing)
|
||
|
||
### 4.1 تصنيف المناطق
|
||
|
||
```
|
||
Amman مقسمة حسب البعد عن المركز (31.95, 35.90):
|
||
|
||
Centre (مركز البلد) → نصف قطر < 2.5km → PPK 0.25-0.33
|
||
Mid (وسط) → نصف قطر < 5km → PPK 0.35-0.45
|
||
Suburb (ضواحي) → نصف قطر < 10km → PPK 0.40-0.50
|
||
Outskirts (أطراف) → > 10km → PPK 0.40-0.60
|
||
```
|
||
|
||
### 4.2 كيف نطبّق Zone-Based Pricing؟
|
||
|
||
بدلاً من معادلة تسعير واحدة لكل البلد، يصبح:
|
||
|
||
```sql
|
||
-- جدول zone_pricing (جديد)
|
||
CREATE TABLE IF NOT EXISTS `zone_pricing` (
|
||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||
`country_code` VARCHAR(5) NOT NULL,
|
||
`zone_type` VARCHAR(20) NOT NULL, -- centre, mid, suburb, outskirts
|
||
`km_rate` DECIMAL(8,3) NOT NULL,
|
||
`min_rate` DECIMAL(8,3) NOT NULL DEFAULT 0,
|
||
`base_fare` DECIMAL(8,3) NOT NULL DEFAULT 0,
|
||
`min_fare` DECIMAL(8,3) NOT NULL DEFAULT 0,
|
||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
UNIQUE KEY `idx_country_zone` (`country_code`, `zone_type`)
|
||
);
|
||
|
||
-- يتم ملؤها من تحليل Pricing Engine
|
||
```
|
||
|
||
ثم في `ride/pricing/get.php`:
|
||
|
||
```php
|
||
// 1. تحديد منطقة البداية
|
||
$zoneType = classifyZone($startLat, $startLng); // centre | mid | suburb | outskirts
|
||
|
||
// 2. استخدام تسعير المنطقة
|
||
$zoneRate = getZonePricing($country, $zoneType);
|
||
$price = $zoneRate['base_fare']
|
||
+ $zoneRate['km_rate'] * $distance
|
||
+ $zoneRate['min_rate'] * $duration;
|
||
|
||
// 3. تطبيق Surge حسب المنطقة
|
||
$surge = getSurgeForZone($country, $zoneType);
|
||
$finalPrice = $price * $surge;
|
||
```
|
||
|
||
### 4.3 إشعارات المناطق للسائقين
|
||
|
||
عند دخول سائق إلى منطقة ذات Surge عالي:
|
||
|
||
```php
|
||
// cron_notify_drivers_zones.php (جديد - كل 5 دقائق)
|
||
$hotZones = getHotZones(); // من Redis surge:opportunities أو تحليل الـ Pricing Engine
|
||
|
||
foreach ($hotZones as $zone) {
|
||
// إرسال FCM notification للسائقين القريبين
|
||
sendPushToNearbyDrivers($zone['lat'], $zone['lng'], [
|
||
'title' => '⚠️ منطقة طلب مرتفع',
|
||
'body' => "منطقة {$zone['name']}: الطلب مرتفع، الأسعار مرتفعة {$zone['surge']}x"
|
||
]);
|
||
}
|
||
```
|
||
|
||
وللراكب عند فتح التطبيق في منطقة Surge:
|
||
|
||
```php
|
||
// في ride/pricing/get.php
|
||
if ($surgeMultiplier > 1.0) {
|
||
$response['surge_warning'] = "⚠️ هذه المنطقة تشهد طلباً مرتفعاً، الأسعار أعلى بنسبة "
|
||
. round(($surgeMultiplier - 1) * 100) . "%";
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. دعم تطبيقات منافسة متعددة
|
||
|
||
### 5.1 لكل منافس معادلاته الخاصة
|
||
|
||
```sql
|
||
-- competitor_secret_formulas يدعم:
|
||
-- competitor_name = 'com.taxif.passenger' | 'com.careem.ae' | 'com.ubercab' | 'com.jeeny.app'
|
||
-- لكل منافس 3 tiers (Economy/Standard/Premium)
|
||
-- لكل tier معادلة مستقلة
|
||
```
|
||
|
||
### 5.2 مقارنة الأسعار في التطبيق
|
||
|
||
```php
|
||
// api/ride/get_competitor_context.php
|
||
$competitors = ['com.taxif.passenger', 'com.careem.ae', 'com.ubercab', 'com.jeeny.app'];
|
||
$prices = [];
|
||
|
||
foreach ($competitors as $comp) {
|
||
$formula = getLatestFormula($comp, $country, 'economy');
|
||
$estimatedPrice = $formula['base_fare']
|
||
+ $formula['price_per_km'] * $requestedDistance
|
||
+ $formula['price_per_min'] * $requestedDuration;
|
||
$prices[$comp] = [
|
||
'name' => getCompetitorDisplayName($comp),
|
||
'price' => $estimatedPrice,
|
||
'currency' => 'JOD',
|
||
];
|
||
}
|
||
|
||
// Siro price (already 6.5% less)
|
||
$siroPrice = calculateSiroPrice($request);
|
||
$prices['siro'] = [
|
||
'name' => 'Siro',
|
||
'price' => $siroPrice,
|
||
'currency' => 'JOD',
|
||
'is_cheapest' => $siroPrice < min(array_column($prices, 'price')),
|
||
];
|
||
```
|
||
|
||
### 5.3 تسعير Siro بناءً على المنافس الأقوى
|
||
|
||
في `cron_ai_engine.php`:
|
||
|
||
```php
|
||
// 1. اجلب معادلات جميع المنافسين للدولة
|
||
$competitors = getCompetitorFormulas($country, 'economy');
|
||
|
||
// 2. احسب السعر المتوقع لكل منافس لرحلة نموذجية (10km, 15min)
|
||
$sampleDist = 10;
|
||
$sampleDur = 15;
|
||
$competitorPrices = [];
|
||
foreach ($competitors as $comp) {
|
||
$competitorPrices[$comp['competitor_name']] =
|
||
$comp['base_fare'] + $comp['price_per_km'] * $sampleDist + $comp['price_per_min'] * $sampleDur;
|
||
}
|
||
|
||
// 3. المنافس الأرخص هو المستهدف
|
||
$cheapestCompetitor = array_keys($competitorPrices, min($competitorPrices))[0];
|
||
$cheapestPrice = min($competitorPrices);
|
||
|
||
// 4. سعر Siro = أرخص منافس - 6.5%
|
||
$targetSiroPrice = $cheapestPrice * 0.935;
|
||
|
||
// 5. هندسة عكسية لمعاملات Siro
|
||
$ourKmRate = $competitors[$cheapestCompetitor]['price_per_km'] * 0.935;
|
||
$ourMinRate = $competitors[$cheapestCompetitor]['price_per_min'] * 0.935;
|
||
$ourBaseFare = $competitors[$cheapestCompetitor]['base_fare'] * 0.935;
|
||
```
|
||
|
||
---
|
||
|
||
## 6. تدفق البيانات من التحليل حتى يشوفها المستخدم
|
||
|
||
```
|
||
الوقت T0: Pricing Engine يشتغل
|
||
↓
|
||
الوقت T0+5s: يكتب competitor_secret_formulas + competitor_surge_insights
|
||
↓
|
||
الوقت T0+30m: cron_ai_engine.php (PHP) يقرأ المعادلات ويحدّث kazan
|
||
↓
|
||
الوقت T0+31m: kazan محدّث بأسعار جديدة (أقل 6.5% من المنافس)
|
||
↓
|
||
الوقت T0+31m+: rider يطلب رحلة
|
||
→ ride/pricing/get.php يقرأ kazan + Redis surge
|
||
→ يحسب السعر ← يرجع للراكب
|
||
→ السائق يشوف سعر الرحلة
|
||
```
|
||
|
||
**المدة الكاملة من التحليل للمستخدم: ~31 دقيقة** (يمكن تقليلها بتشغيل cron_ai_engine بعد Pricing Engine مباشرة).
|
||
|
||
---
|
||
|
||
## 7. متطلبات السيرفر
|
||
|
||
| المكون | المتطلب |
|
||
|---|---|
|
||
| Node.js | v18+ (نوصي v20 LTS) |
|
||
| PM2 | لإدارة العمليات (اختياري) |
|
||
| MySQL | موجود مسبقاً |
|
||
| Redis | موجود مسبقاً |
|
||
| RAM إضافي | 256MB كافية (التطبيق خفيف) |
|
||
| مساحة | 50MB للملفات + node_modules |
|
||
|
||
### أمان: Node.js ما اله Port
|
||
|
||
Pricing Engine هو CLI cron job، مش Web Server. ما اله Port مفتوح. يتصل فقط بـ MySQL و Redis داخلياً. **لا يحتاج تعديل Nginx/Apache**.
|
||
|
||
---
|
||
|
||
## 8. خطة الرفع (Deployment Checklist)
|
||
|
||
```bash
|
||
□ 1. git pull أحدث كود على السيرفر
|
||
□ 2. cd backend/pricing-engine && npm install
|
||
□ 3. cp .env.example .env # عدّل بيانات MySQL + Redis
|
||
□ 4. mysql -u root siro < migrations/001_add_columns.sql
|
||
□ 5. npm run build # compile TypeScript
|
||
□ 6. npm run analyze:taxif # اختبار يدوي
|
||
□ 7. pm2 start ecosystem.config.js # أو crontab
|
||
□ 8. pm2 save && pm2 startup
|
||
□ 9. تحقق من cron_ai_engine.php يقرأ المعادلات الجديدة
|
||
□ 10. اختبر ride/pricing/get.php مع الراكب
|
||
```
|
||
|
||
---
|
||
|
||
## 9. إضافة تطبيق منافس جديد
|
||
|
||
```
|
||
□ 1. أضف اسم الحزمة إلى generate_price_tasks.php
|
||
(مثلاً: com.newcompetitor.app)
|
||
□ 2. انتظر تجميع بيانات كافية (أسبوع scraping)
|
||
□ 3. شغّل: npm run analyze -- --competitor=com.newcompetitor.app
|
||
□ 4. Pricing Engine سيكتشف الـ Tiers تلقائياً
|
||
□ 5. cron_ai_engine.php سيقرأ المعادلات ويطبّق التسعير
|
||
□ 6. تلقائياً: مقارنة الأسعار في تطبيق الراكب
|
||
```
|
||
|
||
---
|
||
|
||
## 10. الخلاصة
|
||
|
||
| الميزة | الحالة |
|
||
|---|---|
|
||
| تحليل إحصائي (MAD + Ridge Regression + K-Means) | ✅ تم |
|
||
| اكتشاف 3 Tiers تسعيرية | ✅ تم |
|
||
| Minimum Fare | ✅ تم |
|
||
| اكتشاف Surge حسب ساعة اليوم | ✅ تم |
|
||
| تحليل Zone | ✅ تم |
|
||
| خصم 6.5% من المنافس | ✅ في cron_ai_engine.php |
|
||
| Redis surge للـ get.php | ✅ surge:opportunities |
|
||
| PK/FK متوافقة | ✅ تم تحديث schema |
|
||
| دعم دول متعددة (JO/SY/EG/IQ) | ✅ Currency-aware |
|
||
| دعم منافسين متعددين | ✅ Arrays + foreach |
|
||
| Zone-Based Pricing | ⬜ يحتاج إنشاء جدول zone_pricing |
|
||
| إشعارات للسائقين بالمناطق الساخنة | ⬜ يحتاج cron_notify_drivers |
|
||
| مقارنة أسعار المنافسين في التطبيق | ⬜ يحتاج ربط ride/pricing مع competitor_formulas |
|
||
|
||
</div>
|