chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل
نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق». نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا `cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules · .dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore. هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ. ⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
# 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>
|
||||
Reference in New Issue
Block a user