Update codebase
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
-- ============================================================
|
||||
-- scraped_competitor_prices: add distance_km / duration_min
|
||||
-- ============================================================
|
||||
-- backend/bot/worker.php computes both values via OSRM
|
||||
-- (core/osrm_routing.php) and has always listed them in its
|
||||
-- INSERT, but the columns were never created — so every
|
||||
-- production bot submission failed with a PDOException.
|
||||
--
|
||||
-- Consumers currently derive distance as
|
||||
-- (price_amount / price_per_km)
|
||||
-- e.g. Admin/marketing/what_if_simulator.php and
|
||||
-- Admin/marketing/trigger_campaign.php
|
||||
-- Those keep working unchanged; the new columns are additive
|
||||
-- and nullable, so historical rows stay valid.
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE `scraped_competitor_prices`
|
||||
ADD COLUMN `distance_km` DECIMAL(8,3) DEFAULT NULL AFTER `price_per_km`,
|
||||
ADD COLUMN `duration_min` DECIMAL(8,2) DEFAULT NULL AFTER `distance_km`;
|
||||
@@ -0,0 +1,36 @@
|
||||
-- ============================================================
|
||||
-- admin_audit_log — جدول سجل تدقيق عمليات الأدمن
|
||||
-- ============================================================
|
||||
-- الجدول كان مستخدماً في الكود ولا وجود له في schema_primary.sql، أي أنه
|
||||
-- غير موجود على أي نشر نظيف. والنتيجة:
|
||||
--
|
||||
-- • logAudit() في backend/functions.php يبتلع الاستثناء ويكتفي بـ
|
||||
-- error_log، فكل عملية تدقيق كانت تفشل بصمت (تحديث كازان، تعديل سائق،
|
||||
-- إرسال إشعار جماعي، تشغيل حملة، تحديث إصدار التطبيق).
|
||||
-- • شاشة "سجلات الأمان" تظهر فارغة دائماً — لا لأن العرض معطّل بل لأن
|
||||
-- الاستعلام لا يجد جدولاً.
|
||||
--
|
||||
-- الأعمدة هنا هي اتحاد ما يكتبه المصدران المختلفان:
|
||||
-- - functions.php::logAudit → admin_id, action, table_name, record_id, details
|
||||
-- - Admin/v2/quality/blacklist_manager.php → admin_id, admin_phone, action, table_name, entity_type, details
|
||||
-- ولذلك فالأعمدة غير المشتركة تقبل NULL.
|
||||
--
|
||||
-- ملاحظة الترميز: audit_logs.php يربط admin_id مع employee.id و adminUser.id
|
||||
-- عبر COLLATE utf8mb4_general_ci صراحةً، فنُثبّت الترميز نفسه هنا لتفادي
|
||||
-- خطأ "Illegal mix of collations" عند الربط.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `admin_audit_log` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`admin_id` VARCHAR(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
|
||||
`admin_phone` VARCHAR(32) DEFAULT NULL,
|
||||
`action` VARCHAR(255) NOT NULL,
|
||||
`table_name` VARCHAR(100) DEFAULT NULL,
|
||||
`entity_type` VARCHAR(100) DEFAULT NULL,
|
||||
`record_id` VARCHAR(100) DEFAULT NULL,
|
||||
`details` TEXT DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY `idx_admin_id` (`admin_id`),
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
KEY `idx_action` (`action`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
@@ -0,0 +1,50 @@
|
||||
-- ============================================================
|
||||
-- marketing_tasks + marketing_reports — جدولا محرّك التسويق والبوت الاجتماعي
|
||||
-- ============================================================
|
||||
-- كلا الجدولين مستعمل في الكود ولا وجود لهما في schema_primary.sql — نفس
|
||||
-- حالة admin_audit_log بالضبط. الأثر:
|
||||
--
|
||||
-- • marketing_tasks هو طابور مهام البوت الاجتماعي:
|
||||
-- يكتب فيه cron_insert_task.php (كل 4 ساعات) و insert_autonomous_task.php
|
||||
-- ويقرأ منه marketing_engine/index.php ليخدم socialBot.
|
||||
-- بغيابه لا يستلم البوت مهمة واحدة أبداً، ويفشل الكرون بصمت.
|
||||
--
|
||||
-- • marketing_reports هو مصدر شاشة "الذكاء الاجتماعي":
|
||||
-- يكتب فيه telegram_scraper.php (كل 3 ساعات) و cron_weekly_report.php
|
||||
-- و index.php (evaluate_posts).
|
||||
-- بغيابه تظهر الشاشة فارغة دائماً — وهو ما لوحظ.
|
||||
--
|
||||
-- الأعمدة مستخرجة من مواضع الاستعمال الفعلية في:
|
||||
-- marketing_engine/index.php, cron_insert_task.php, cron_weekly_report.php,
|
||||
-- insert_autonomous_task.php, telegram_scraper.php, insert_test_task.php
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `marketing_tasks` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`platform` VARCHAR(50) NOT NULL DEFAULT 'facebook',
|
||||
`type` VARCHAR(100) NOT NULL,
|
||||
-- 'pending' هي الحالة التي يبحث عنها get_task؛ أي قيمة أخرى تعني
|
||||
-- أن المهمة قيد التنفيذ أو انتهت.
|
||||
`status` VARCHAR(30) NOT NULL DEFAULT 'pending',
|
||||
`target_url` TEXT DEFAULT NULL,
|
||||
`content_text` TEXT DEFAULT NULL,
|
||||
`media_url` TEXT DEFAULT NULL,
|
||||
`error_message` TEXT DEFAULT NULL,
|
||||
`completed_at` TIMESTAMP NULL DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
-- get_task يرشّح بـ status + platform ويرتّب بـ id
|
||||
KEY `idx_status_platform` (`status`, `platform`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `marketing_reports` (
|
||||
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
`platform` VARCHAR(50) NOT NULL DEFAULT 'all',
|
||||
`report_html` LONGTEXT NOT NULL,
|
||||
-- cron_weekly_report.php يكتب 1؛ التقارير اللحظية تترك 0
|
||||
`is_weekly` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
-- get_reports يرتّب بـ created_at تنازلياً
|
||||
KEY `idx_created_at` (`created_at`),
|
||||
KEY `idx_is_weekly` (`is_weekly`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,75 @@
|
||||
-- 2026_08_07_complaint_resolution.sql
|
||||
--
|
||||
-- دورة الشكوى وأداة التعويض.
|
||||
--
|
||||
-- الوضع اليوم: serviceapp/update_complaint.php يقلب الحالة ويكتب نصّ
|
||||
-- قرار. لا تعويض، لا تصنيف سبب، ولا قياس زمن. والموظف الذي يريد
|
||||
-- تعويض راكب متضرر لا يملك أي أداة — فيَعِد شفهياً أو لا يفعل شيئاً.
|
||||
--
|
||||
-- كلمة "refund" لا ترد إطلاقاً في مسار الرحلات، فقط في وحدة الطعام.
|
||||
--
|
||||
-- هذا الترحيل يحوّل الدعم من "استقبال شكاوى" إلى "إغلاق قضايا".
|
||||
--
|
||||
-- ⚠️ يُنفَّذ على قاعدة primary (jorSiroDB) وحدها.
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ١) قياس الزمن (SLA)
|
||||
--
|
||||
-- date_resolved موجود أصلاً، لكن زمن **أول رد** هو المؤشر الذي يشعر به
|
||||
-- العميل فعلاً — شكوى تُغلق بعد يومين بردٍّ خلال دقائق أفضل بكثير من
|
||||
-- شكوى تُغلق بعد ساعة بلا أن يعلم صاحبها شيئاً.
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
ALTER TABLE `complaint`
|
||||
ADD COLUMN `first_response_at` DATETIME NULL DEFAULT NULL
|
||||
COMMENT 'لحظة أول رد من الدعم — يُختم مرة واحدة ولا يتغيّر',
|
||||
ADD COLUMN `assigned_to` VARCHAR(100) NULL DEFAULT NULL
|
||||
COMMENT 'معرّف الموظف المسؤول عن القضية',
|
||||
ADD COLUMN `reason_code` VARCHAR(40) NULL DEFAULT NULL
|
||||
COMMENT 'تصنيف موحّد للسبب — قائمة مغلقة في resolve_complaint.php',
|
||||
ADD INDEX `idx_status_filed` (`statusComplaint`, `date_filed`),
|
||||
ADD INDEX `idx_ride` (`ride_id`);
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ٢) سجل التعويضات
|
||||
--
|
||||
-- جدول مستقل لا أعمدة في complaint: الشكوى الواحدة قد تُعوَّض أكثر من
|
||||
-- مرة (استرجاع جزئي ثم رصيد اعتذار)، وكل حركة مالية تحتاج أثراً مستقلاً
|
||||
-- يُراجَع ويُدقَّق.
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
CREATE TABLE IF NOT EXISTS `complaint_compensations` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`complaint_id` INT NOT NULL,
|
||||
`ride_id` VARCHAR(20) NULL DEFAULT NULL,
|
||||
|
||||
`beneficiary_type` VARCHAR(20) NOT NULL
|
||||
COMMENT 'passenger | driver — لمن ذهب التعويض',
|
||||
`beneficiary_id` VARCHAR(100) NOT NULL,
|
||||
|
||||
`kind` VARCHAR(20) NOT NULL
|
||||
COMMENT 'refund (استرجاع من قيمة الرحلة) | goodwill (رصيد اعتذار)',
|
||||
`amount` DECIMAL(12,3) NOT NULL,
|
||||
`currency` VARCHAR(5) NOT NULL DEFAULT 'SYP',
|
||||
|
||||
`issued_by` VARCHAR(100) NOT NULL COMMENT 'الموظف الذي صرفه',
|
||||
`note` VARCHAR(500) NULL DEFAULT NULL,
|
||||
|
||||
-- نتيجة نداء المحفظة. الفشل يبقى مسجَّلاً للتسوية اليدوية بدل أن
|
||||
-- يختفي في اللوجز — العميل وُعد بمبلغ ويجب أن يصله.
|
||||
`transfer_status` VARCHAR(20) NOT NULL DEFAULT 'pending'
|
||||
COMMENT 'pending | success | failed',
|
||||
`transfer_code` SMALLINT NULL DEFAULT NULL,
|
||||
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_complaint` (`complaint_id`),
|
||||
KEY `idx_beneficiary` (`beneficiary_type`, `beneficiary_id`),
|
||||
KEY `idx_failed` (`transfer_status`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||
COMMENT='تعويضات الشكاوى — كل حركة مالية بأثر مستقل قابل للتدقيق';
|
||||
|
||||
-- للتراجع:
|
||||
-- ALTER TABLE `complaint` DROP COLUMN `first_response_at`,
|
||||
-- DROP COLUMN `assigned_to`, DROP COLUMN `reason_code`,
|
||||
-- DROP INDEX `idx_status_filed`, DROP INDEX `idx_ride`;
|
||||
-- DROP TABLE `complaint_compensations`;
|
||||
@@ -0,0 +1,59 @@
|
||||
-- 2026_08_07_driver_cancel_compensation.sql
|
||||
--
|
||||
-- تعويض السائق عند عدم حضور الراكب (إلغاء السائق بعد الوصول والانتظار).
|
||||
-- النموذج: التعويض = مسافة الاقتراب إلى الراكب × سعر الكيلومتر لنوع السيارة
|
||||
-- + دقائق الانتظار الفعلية × سعر الدقيقة حسب وقت اليوم.
|
||||
-- يُخصم من محفظة الراكب ويُضاف لمحفظة السائق. الأسعار من kazan نفسه الذي
|
||||
-- تُسعَّر به الرحلة، عبر ride/pricing/pricing_helper.php.
|
||||
--
|
||||
-- يستبدل هذا الملف مقاربة "الرسم الثابت" السابقة: المبلغ لم يعد يُقرأ من
|
||||
-- طلب العميل (كان filterRequest("penalty_fee") بلا سقف ولا تحقق)، ولم يعد
|
||||
-- رقماً ثابتاً — صار محسوباً من بيانات مسجَّلة على الخادم.
|
||||
--
|
||||
-- ⚠️ هذا الملف لقاعدة primary (jorSiroDB) حصراً. الملف التوأم _ride.sql يُنفَّذ على قاعدة الرحلات (intaleq-ridesDB).
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ١) سياسة التعويض لكل دولة (قاعدة primary فقط — kazan يُقرأ عبر $con)
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
ALTER TABLE `kazan`
|
||||
ADD COLUMN `cancelCompensationCap` VARCHAR(10)
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
|
||||
NOT NULL DEFAULT '0'
|
||||
COMMENT 'سقف تعويض عدم حضور الراكب بعملة الدولة. 0 = التعويض معطّل بالكامل'
|
||||
AFTER `startPrice`,
|
||||
ADD COLUMN `cancelMinWaitMinutes` TINYINT UNSIGNED
|
||||
NOT NULL DEFAULT 5
|
||||
COMMENT 'أقل دقائق انتظار بعد الوصول قبل أن يستحق السائق تعويضاً'
|
||||
AFTER `cancelCompensationCap`;
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ٢) بيانات الوصول والاقتراب (قاعدتا primary و ride)
|
||||
--
|
||||
-- هذان العمودان للتوثيق النهائي فقط ولا يُكتبان أثناء الرحلة: القيمتان
|
||||
-- تعيشان في هاش Redis نفسه الذي يستخدمه السوكِت لحالة الرحلة
|
||||
-- (ride:$id:state، الحقول pickup_approach_km و driver_arrived_at)،
|
||||
-- وتُثبَّتان هنا كتابةً واحدة مدمجة في جملة تحديث الحالة عند الإلغاء.
|
||||
-- أي: صفر كتابات إضافية على المسار الحار (القبول والوصول).
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
ALTER TABLE `ride`
|
||||
ADD COLUMN `driverArrivedAt` DATETIME NULL DEFAULT NULL
|
||||
COMMENT 'لحظة تأكيد وصول السائق لنقطة الالتقاط — يختمها الخادم بعد تحقق الموقع',
|
||||
ADD COLUMN `pickupApproachKm` FLOAT NOT NULL DEFAULT 0
|
||||
COMMENT 'المسافة من موقع السائق لحظة القبول إلى نقطة الالتقاط (كم)';
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ٣) تطبيق الإيقاف على الخادم (قاعدتا primary و ride)
|
||||
-- كان الإيقاف يُكتب في تخزين تطبيق السائق وحده، فيُفكّ بإعادة التثبيت.
|
||||
--
|
||||
-- مسار الفحص في acceptRide.php هو مفتاح Redis بـ TTL (driver:$id:blocked)
|
||||
-- فينتهي الإيقاف تلقائياً بلا كرون تنظيف وبلا استعلام على كل قبول.
|
||||
-- هذا العمود سجل دائم للمراجعة الإدارية، يُكتب في الإلغاء الثالث فقط.
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
ALTER TABLE `driver`
|
||||
ADD COLUMN `blockedUntil` DATETIME NULL DEFAULT NULL
|
||||
COMMENT 'إيقاف استقبال الرحلات حتى هذا الوقت (إلغاءات متكررة). NULL = غير موقوف';
|
||||
|
||||
-- للتراجع:
|
||||
-- ALTER TABLE `kazan` DROP COLUMN `cancelCompensationCap`, DROP COLUMN `cancelMinWaitMinutes`;
|
||||
-- ALTER TABLE `ride` DROP COLUMN `driverArrivedAt`, DROP COLUMN `pickupApproachKm`;
|
||||
-- ALTER TABLE `driver` DROP COLUMN `blockedUntil`;
|
||||
@@ -0,0 +1,44 @@
|
||||
-- 2026_08_07_driver_cancel_compensation.sql
|
||||
--
|
||||
-- تعويض السائق عند عدم حضور الراكب (إلغاء السائق بعد الوصول والانتظار).
|
||||
-- النموذج: التعويض = مسافة الاقتراب إلى الراكب × سعر الكيلومتر لنوع السيارة
|
||||
-- + دقائق الانتظار الفعلية × سعر الدقيقة حسب وقت اليوم.
|
||||
-- يُخصم من محفظة الراكب ويُضاف لمحفظة السائق. الأسعار من kazan نفسه الذي
|
||||
-- تُسعَّر به الرحلة، عبر ride/pricing/pricing_helper.php.
|
||||
--
|
||||
-- يستبدل هذا الملف مقاربة "الرسم الثابت" السابقة: المبلغ لم يعد يُقرأ من
|
||||
-- طلب العميل (كان filterRequest("penalty_fee") بلا سقف ولا تحقق)، ولم يعد
|
||||
-- رقماً ثابتاً — صار محسوباً من بيانات مسجَّلة على الخادم.
|
||||
--
|
||||
-- ⚠️ هذا الملف لقاعدة ride (intaleq-ridesDB) حصراً. جدول kazan لا يُمسّ هنا: بنيته في قاعدة الرحلات أقدم ومختلفة الأعمدة، ولا يُقرأ في هذا المسار.
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ٢) بيانات الوصول والاقتراب (قاعدتا primary و ride)
|
||||
--
|
||||
-- هذان العمودان للتوثيق النهائي فقط ولا يُكتبان أثناء الرحلة: القيمتان
|
||||
-- تعيشان في هاش Redis نفسه الذي يستخدمه السوكِت لحالة الرحلة
|
||||
-- (ride:$id:state، الحقول pickup_approach_km و driver_arrived_at)،
|
||||
-- وتُثبَّتان هنا كتابةً واحدة مدمجة في جملة تحديث الحالة عند الإلغاء.
|
||||
-- أي: صفر كتابات إضافية على المسار الحار (القبول والوصول).
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
ALTER TABLE `ride`
|
||||
ADD COLUMN `driverArrivedAt` DATETIME NULL DEFAULT NULL
|
||||
COMMENT 'لحظة تأكيد وصول السائق لنقطة الالتقاط — يختمها الخادم بعد تحقق الموقع',
|
||||
ADD COLUMN `pickupApproachKm` FLOAT NOT NULL DEFAULT 0
|
||||
COMMENT 'المسافة من موقع السائق لحظة القبول إلى نقطة الالتقاط (كم)';
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ٣) تطبيق الإيقاف على الخادم (قاعدتا primary و ride)
|
||||
-- كان الإيقاف يُكتب في تخزين تطبيق السائق وحده، فيُفكّ بإعادة التثبيت.
|
||||
--
|
||||
-- مسار الفحص في acceptRide.php هو مفتاح Redis بـ TTL (driver:$id:blocked)
|
||||
-- فينتهي الإيقاف تلقائياً بلا كرون تنظيف وبلا استعلام على كل قبول.
|
||||
-- هذا العمود سجل دائم للمراجعة الإدارية، يُكتب في الإلغاء الثالث فقط.
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
ALTER TABLE `driver`
|
||||
ADD COLUMN `blockedUntil` DATETIME NULL DEFAULT NULL
|
||||
COMMENT 'إيقاف استقبال الرحلات حتى هذا الوقت (إلغاءات متكررة). NULL = غير موقوف';
|
||||
|
||||
-- للتراجع:
|
||||
-- ALTER TABLE `ride` DROP COLUMN `driverArrivedAt`, DROP COLUMN `pickupApproachKm`;
|
||||
-- ALTER TABLE `driver` DROP COLUMN `blockedUntil`;
|
||||
@@ -0,0 +1,116 @@
|
||||
-- ============================================================
|
||||
-- التأمين للسائق — بند 3.5 من دراسة الفرص
|
||||
--
|
||||
-- ما كان موجوداً قبل هذا: جدول `driver_health_assurance` فيه بوليان
|
||||
-- `assured` واسم شركة نصّاً، وملف add.php وحيد بلا حارس مصادقة —
|
||||
-- و get.php و update.php **فارغان تماماً** (صفر بايت). أي أن الفكرة
|
||||
-- لم "تبدأ وتتوقف" كما تقول الدراسة، بل لم تبدأ.
|
||||
--
|
||||
-- قرار المالك: شريك مرخّص (ميتلايف وآخر في مصر)، قسط يومي أو شهري
|
||||
-- يُخصم من أرباح السائق، وأهلية مشروطة بعدد رحلات وتقييم — التأمين
|
||||
-- مكافأة استمرار لا خدمة تُمنح لكل من سجّل.
|
||||
--
|
||||
-- لا نلمس driver_health_assurance: يبقى كما هو لمن سُجّل فيه سابقاً،
|
||||
-- والجداول هنا هي المسار الجديد.
|
||||
-- ============================================================
|
||||
|
||||
-- ── ١) شركات التأمين ────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS `insurance_providers` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(120) NOT NULL COMMENT 'MetLife وغيرها',
|
||||
`name_ar` VARCHAR(120) DEFAULT NULL,
|
||||
`country` VARCHAR(60) NOT NULL DEFAULT 'Egypt',
|
||||
`contact` VARCHAR(255) DEFAULT NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_country_active` (`country`, `is_active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── ٢) الخطط ────────────────────────────────────────────────
|
||||
-- شروط الأهلية تُخزَّن مع الخطة لا في الكود: الشريك سيغيّرها، والسوق
|
||||
-- المصري يختلف عن الأردني، وتغيير رقم في صف أرخص من نشر إصدار.
|
||||
CREATE TABLE IF NOT EXISTS `insurance_plans` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`provider_id` INT NOT NULL,
|
||||
`code` VARCHAR(60) NOT NULL COMMENT 'معرّف ثابت يُستعمل في الكود',
|
||||
`name_ar` VARCHAR(160) NOT NULL,
|
||||
`description_ar` TEXT DEFAULT NULL,
|
||||
`billing_cycle` ENUM('daily','monthly') NOT NULL DEFAULT 'daily',
|
||||
`premium` DECIMAL(10,3) NOT NULL COMMENT 'قسط الدورة الواحدة',
|
||||
`currency` VARCHAR(10) NOT NULL DEFAULT 'EGP',
|
||||
`coverage_summary` TEXT DEFAULT NULL,
|
||||
|
||||
-- ── شروط الأهلية ──
|
||||
-- قرار المالك: لا تأمين قبل ~٢٠٠ رحلة بتقييم جيد. المنطق سليم —
|
||||
-- التأمين كلفة متكررة على الشركة، ومنحه لمن قد يختفي بعد أسبوع
|
||||
-- يحوّله من أداة ولاء إلى نزيف.
|
||||
`min_completed_rides` INT NOT NULL DEFAULT 200,
|
||||
`min_rating` DECIMAL(3,2) NOT NULL DEFAULT 4.50,
|
||||
`min_account_days` INT NOT NULL DEFAULT 30,
|
||||
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_code` (`code`),
|
||||
KEY `idx_provider_active` (`provider_id`, `is_active`),
|
||||
CONSTRAINT `fk_plan_provider` FOREIGN KEY (`provider_id`)
|
||||
REFERENCES `insurance_providers` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── ٣) وثائق السائقين ───────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS `driver_insurance_policies` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`driver_id` VARCHAR(100) NOT NULL,
|
||||
`plan_id` INT NOT NULL,
|
||||
`policy_number` VARCHAR(120) DEFAULT NULL COMMENT 'رقم الوثيقة من الشريك',
|
||||
`status` ENUM('active','suspended','cancelled','expired')
|
||||
NOT NULL DEFAULT 'active',
|
||||
`started_at` DATE NOT NULL,
|
||||
`ended_at` DATE DEFAULT NULL,
|
||||
|
||||
-- لقطة الأهلية لحظة الاشتراك. بدونها لا يمكن الإجابة لاحقاً على
|
||||
-- «لماذا قُبل هذا السائق؟» حين تتغيّر شروط الخطة أو ينخفض تقييمه.
|
||||
`rides_at_signup` INT NOT NULL DEFAULT 0,
|
||||
`rating_at_signup` DECIMAL(3,2) NOT NULL DEFAULT 0,
|
||||
|
||||
-- آخر يوم حُسب عنه قسط. هذا العمود — لا التاريخ الحالي — هو ما
|
||||
-- يمنع الاحتساب المزدوج لو عمل الكرون مرتين في اليوم نفسه.
|
||||
`last_charged_on` DATE DEFAULT NULL,
|
||||
`cancel_reason` VARCHAR(255) DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
-- وثيقة واحدة نشطة لكل سائق. المفتاح الفريد على (driver_id, status)
|
||||
-- لا يصلح — يمنع وثيقتين ملغيتين. نمنعه في الكود ونكشفه بفهرس.
|
||||
KEY `idx_driver_status` (`driver_id`, `status`),
|
||||
KEY `idx_charge_sweep` (`status`, `last_charged_on`),
|
||||
CONSTRAINT `fk_policy_plan` FOREIGN KEY (`plan_id`)
|
||||
REFERENCES `insurance_plans` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── ٤) دفتر الأقساط ─────────────────────────────────────────
|
||||
-- دفتر لا خصم مباشر. رصيد السائق يعيش في payment_server لا هنا،
|
||||
-- فالكرون يقيّد استحقاقاً والتسوية تقرأه. الفصل مقصود أيضاً لسبب
|
||||
-- محاسبي: القيد سجلّ دائم قابل للمراجعة، بينما الخصم المباشر يترك
|
||||
-- رقماً متغيّراً بلا أثر يشرح كيف صار كذلك.
|
||||
CREATE TABLE IF NOT EXISTS `insurance_premium_ledger` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`policy_id` INT NOT NULL,
|
||||
`driver_id` VARCHAR(100) NOT NULL,
|
||||
`charge_date` DATE NOT NULL COMMENT 'اليوم أو أول الشهر المحتسَب',
|
||||
`amount` DECIMAL(10,3) NOT NULL,
|
||||
`currency` VARCHAR(10) NOT NULL DEFAULT 'EGP',
|
||||
`status` ENUM('pending','settled','waived','failed')
|
||||
NOT NULL DEFAULT 'pending',
|
||||
`settled_at` DATETIME DEFAULT NULL,
|
||||
`note` VARCHAR(255) DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
-- الحارس الحقيقي ضد الاحتساب المزدوج: قسط واحد لكل وثيقة في اليوم
|
||||
-- الواحد مهما تكرّر تشغيل الكرون أو تزامنت نسختان منه.
|
||||
UNIQUE KEY `uq_policy_date` (`policy_id`, `charge_date`),
|
||||
KEY `idx_driver_status` (`driver_id`, `status`),
|
||||
CONSTRAINT `fk_ledger_policy` FOREIGN KEY (`policy_id`)
|
||||
REFERENCES `driver_insurance_policies` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,50 @@
|
||||
-- 2026_08_07_leak_detection.sql
|
||||
--
|
||||
-- كشف التسرب خارج المنصة — سجل خادمي مستقل عن بلاغ التطبيق.
|
||||
--
|
||||
-- الموجود اليوم: جدول driver_ride_scam يملؤه **تطبيق السائق عن نفسه**
|
||||
-- (map_driver_controller.dart:823). نسخة معدّلة من التطبيق ببساطة لا
|
||||
-- ترسل البلاغ، فالكشف يعتمد على تعاون من نريد كشفه. ويبقى مفيداً كإشارة
|
||||
-- إضافية، لذلك لا نلمسه.
|
||||
--
|
||||
-- الجديد: الخادم يستنتج النمط بنفسه من بيانات لا يملك السائق تزويرها —
|
||||
-- قَبِل، ثم أُلغيت، ولم تبدأ الرحلة قط. وتكرار نفس زوج (سائق، راكب) هو
|
||||
-- الإشارة عالية الثقة: صدفةً مرة، ونمطاً مرتين.
|
||||
--
|
||||
-- ⚠️ يُنفَّذ على قاعدة primary (jorSiroDB) وحدها.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `driver_leak_events` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`driver_id` VARCHAR(100) NOT NULL,
|
||||
`passenger_id` VARCHAR(100) NOT NULL,
|
||||
`ride_id` VARCHAR(20) NOT NULL,
|
||||
`cancel_status` VARCHAR(40) NOT NULL
|
||||
COMMENT 'الحالة التي انتهت بها الرحلة (CancelFromDriverAfterApply / cancelled_by_passenger)',
|
||||
`pair_repeat` SMALLINT UNSIGNED NOT NULL DEFAULT 1
|
||||
COMMENT 'كم مرة تكرر نفس زوج (سائق، راكب) بهذا النمط خلال النافذة',
|
||||
`app_reported` TINYINT(1) NOT NULL DEFAULT 0
|
||||
COMMENT 'هل بلّغ تطبيق السائق أيضاً (driver_ride_scam)؟ تطابقهما يرفع الثقة',
|
||||
`confidence` DECIMAL(4,3) NOT NULL DEFAULT 0
|
||||
COMMENT 'درجة الثقة 0..1 — يحسبها bot/cron_leak_detection.php',
|
||||
`reviewed_by` VARCHAR(100) NULL DEFAULT NULL
|
||||
COMMENT 'معرّف الإداري الذي راجع الحالة. NULL = لم تُراجع بعد',
|
||||
`review_note` VARCHAR(255) NULL DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
-- يمنع الكرون من تسجيل نفس الرحلة مرتين عند كل مرور
|
||||
UNIQUE KEY `uniq_ride` (`ride_id`),
|
||||
KEY `idx_driver` (`driver_id`, `created_at`),
|
||||
KEY `idx_pair` (`driver_id`, `passenger_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||
COMMENT='أحداث تسرب مشتبهة يستنتجها الخادم — لا يبلّغ عنها التطبيق';
|
||||
|
||||
-- driver_orders يُقرأ في هذا الكشف بالانضمام على order_id و driver_id،
|
||||
-- وهو اليوم بلا أي فهرس غير المفتاح الأساسي (id). الفحص كان سيمسح
|
||||
-- الجدول كاملاً عند كل مرور.
|
||||
ALTER TABLE `driver_orders`
|
||||
ADD INDEX `idx_order` (`order_id`),
|
||||
ADD INDEX `idx_driver_created` (`driver_id`, `created_at`);
|
||||
|
||||
-- للتراجع:
|
||||
-- DROP TABLE `driver_leak_events`;
|
||||
-- ALTER TABLE `driver_orders` DROP INDEX `idx_order`, DROP INDEX `idx_driver_created`;
|
||||
@@ -0,0 +1,41 @@
|
||||
-- 2026_08_07_passenger_cancel_policy.sql
|
||||
--
|
||||
-- رسوم إلغاء الراكب بعد قبول السائق.
|
||||
--
|
||||
-- متماثلة مع تعويض عدم الحضور (ترحيل 2026_08_07_driver_cancel_compensation):
|
||||
-- نفس المبدأ — من تسبب بضياع وقت الآخر يعوّضه، والمال يذهب **للسائق** لا
|
||||
-- للشركة. الفرق أن الزمن المحتسب هنا يبدأ من لحظة القبول لا من لحظة الوصول،
|
||||
-- لأن السائق يكون في طريقه لا واقفاً ينتظر.
|
||||
--
|
||||
-- ثلاثة إعفاءات مبنية عمداً، لأن رسماً بلا إعفاء يعاقب الراكب على أخطاء
|
||||
-- ليست خطأه ويطرده من التطبيق:
|
||||
-- ١. لا سائق قُبِل بعد → مجاني دائماً
|
||||
-- ٢. خلال أول دقائق من القبول → مجاني (passengerCancelFreeMinutes)
|
||||
-- ٣. السائق تأخّر ولم يصل → مجاني (driverLateExemptMinutes)
|
||||
--
|
||||
-- ⚠️ يُنفَّذ على قاعدة primary (jorSiroDB) وحدها — kazan يُقرأ عبر $con.
|
||||
|
||||
ALTER TABLE `kazan`
|
||||
ADD COLUMN `passengerCancelCap` VARCHAR(10)
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
|
||||
NOT NULL DEFAULT '0'
|
||||
COMMENT 'سقف رسم إلغاء الراكب بعملة الدولة. 0 = الرسم معطّل بالكامل'
|
||||
AFTER `cancelMinWaitMinutes`,
|
||||
ADD COLUMN `passengerCancelFreeMinutes` TINYINT UNSIGNED
|
||||
NOT NULL DEFAULT 2
|
||||
COMMENT 'دقائق الإلغاء المجاني بعد قبول السائق'
|
||||
AFTER `passengerCancelCap`,
|
||||
ADD COLUMN `driverLateExemptMinutes` TINYINT UNSIGNED
|
||||
NOT NULL DEFAULT 10
|
||||
COMMENT 'إن مرّت هذه الدقائق على القبول ولم يصل السائق، الإلغاء مجاني';
|
||||
|
||||
-- عدّاد عدم الحضور للراكب — سجل دائم للمراجعة الإدارية.
|
||||
-- (الفحص السريع يتم من Redis، وهذا العمود للتقارير وقرارات الحظر اليدوية.)
|
||||
ALTER TABLE `passengers`
|
||||
ADD COLUMN `noShowCount` SMALLINT UNSIGNED NOT NULL DEFAULT 0
|
||||
COMMENT 'مرات إلغاء الراكب بعد قبول السائق ودفعه رسماً';
|
||||
|
||||
-- للتراجع:
|
||||
-- ALTER TABLE `kazan` DROP COLUMN `passengerCancelCap`,
|
||||
-- DROP COLUMN `passengerCancelFreeMinutes`, DROP COLUMN `driverLateExemptMinutes`;
|
||||
-- ALTER TABLE `passengers` DROP COLUMN `noShowCount`;
|
||||
@@ -0,0 +1,63 @@
|
||||
-- 2026_08_07_safety_incidents.sql
|
||||
--
|
||||
-- غرفة عمليات السلامة — أثر يمكن مراجعته لواقعة حقيقية.
|
||||
--
|
||||
-- الوضع اليوم: زر SOS في تطبيق الراكب ينتهي عند رسالة واتساب لجهة اتصال
|
||||
-- شخصية خزّنها الراكب بنفسه (ui_interactions_controller.dart:145). لا نداء
|
||||
-- API، لا سجل، لا تنبيه لأحد داخل الشركة. لو وقعت حادثة فعلية، الشركة لا
|
||||
-- تعرف بها ولا تملك دليلاً عنها ولا تستطيع التدخل.
|
||||
--
|
||||
-- هذا الجدول هو الأثر المفقود. رسالة الواتساب تبقى كما هي — تُرسل بالتوازي
|
||||
-- لا بدلاً، لأنها الأسرع وصولاً لمن يهمّه الأمر فعلاً.
|
||||
--
|
||||
-- ⚠️ يُنفَّذ على قاعدة primary (jorSiroDB) وحدها.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `safety_incidents` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
|
||||
-- من أبلغ ولماذا
|
||||
`reporter_type` VARCHAR(20) NOT NULL
|
||||
COMMENT 'passenger | driver — من ضغط الزر',
|
||||
`reporter_id` VARCHAR(100) NOT NULL,
|
||||
`trigger_type` VARCHAR(30) NOT NULL DEFAULT 'sos_button'
|
||||
COMMENT 'sos_button | shake | route_deviation | long_stop | overtime',
|
||||
|
||||
-- سياق الواقعة
|
||||
`ride_id` VARCHAR(20) NULL DEFAULT NULL,
|
||||
`driver_id` VARCHAR(100) NULL DEFAULT NULL,
|
||||
`passenger_id` VARCHAR(100) NULL DEFAULT NULL,
|
||||
`lat` DECIMAL(10,7) NULL DEFAULT NULL,
|
||||
`lng` DECIMAL(10,7) NULL DEFAULT NULL,
|
||||
`note` VARCHAR(500) NULL DEFAULT NULL,
|
||||
|
||||
-- لقطة الحالة وقت الواقعة، مجمّدة كما كانت.
|
||||
-- الرحلة تتغيّر بعد الحادثة (تُلغى، تُنهى، يُصفَّر السائق) فقراءتها لاحقاً
|
||||
-- لا تصف اللحظة. هذه اللقطة هي الدليل.
|
||||
`snapshot` JSON NULL DEFAULT NULL,
|
||||
|
||||
-- دورة المعالجة
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'open'
|
||||
COMMENT 'open | acknowledged | resolved | false_alarm',
|
||||
`acknowledged_by` VARCHAR(100) NULL DEFAULT NULL,
|
||||
`acknowledged_at` DATETIME NULL DEFAULT NULL,
|
||||
`resolution_note` VARCHAR(500) NULL DEFAULT NULL,
|
||||
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_status_created` (`status`, `created_at`),
|
||||
KEY `idx_ride` (`ride_id`),
|
||||
KEY `idx_driver` (`driver_id`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||
COMMENT='بلاغات السلامة — السجل الوحيد القابل للمراجعة لواقعة';
|
||||
|
||||
-- تجميد إسناد الرحلات للسائق حتى المراجعة.
|
||||
-- منفصل عن blockedUntil (عقوبة الإلغاءات المتكررة): ذاك مؤقت بساعات
|
||||
-- وينتهي وحده، وهذا يبقى حتى يرفعه إنسان.
|
||||
ALTER TABLE `driver`
|
||||
ADD COLUMN `safetyHoldIncidentId` INT NULL DEFAULT NULL
|
||||
COMMENT 'رقم بلاغ السلامة الذي جمّد هذا السائق. NULL = غير مجمّد';
|
||||
|
||||
-- للتراجع:
|
||||
-- DROP TABLE `safety_incidents`;
|
||||
-- ALTER TABLE `driver` DROP COLUMN `safetyHoldIncidentId`;
|
||||
@@ -0,0 +1,65 @@
|
||||
-- 2026_08_07_scheduled_rides.sql — الحجز المسبق
|
||||
--
|
||||
-- شريحة مفقودة بالكامل: رحلات المطار، المواعيد الطبية، الدوام اليومي،
|
||||
-- رحلات الفجر. أعلى الشرائح استعداداً للدفع وأقلها حساسية للسعر.
|
||||
--
|
||||
-- والزاوية الأهم: الحجز ليس ميزة راكب فقط — هو **مصدر توقّع طلب مؤكد**.
|
||||
-- رحلة محجوزة لبعد ست ساعات معلومة يقينية تُغذّي cron_predictive_demand
|
||||
-- والخريطة الحرارية بشيء أدق من أي تنبؤ إحصائي، وتسمح بتوجيه السائقين
|
||||
-- قبل الذروة لا بعدها.
|
||||
--
|
||||
-- ⚠️ يُنفَّذ على قاعدة primary (jorSiroDB) وحدها: الكرون والتنبؤ يقرآن
|
||||
-- من primary، والرحلة الفعلية تُنشأ عبر add_ride.php الذي يكتب للاثنتين.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `scheduled_rides` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`passenger_id` VARCHAR(100) NOT NULL,
|
||||
|
||||
-- نقاط الرحلة — نفس صيغة ride.start_location ("lat,lng")
|
||||
`start_location` VARCHAR(255) NOT NULL,
|
||||
`end_location` VARCHAR(255) NOT NULL,
|
||||
`start_name` VARCHAR(255) NULL DEFAULT NULL,
|
||||
`end_name` VARCHAR(255) NULL DEFAULT NULL,
|
||||
|
||||
`car_type` VARCHAR(20) NOT NULL DEFAULT 'Speed',
|
||||
`distance` FLOAT NOT NULL DEFAULT 0,
|
||||
`duration` INT NOT NULL DEFAULT 0 COMMENT 'ثوانٍ',
|
||||
|
||||
-- السعر التقديري وقت الحجز. ليس ملزماً: التسعير النهائي يحدث لحظة
|
||||
-- إنشاء الرحلة الفعلية، فسعر ما قبل ست ساعات لا يعرف الذروة ولا الطقس.
|
||||
`estimated_price` DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
|
||||
`scheduled_at` DATETIME NOT NULL COMMENT 'موعد انطلاق الرحلة كما طلبه الراكب',
|
||||
|
||||
-- كم دقيقة قبل الموعد نبدأ البحث عن سائق. رحلة مطار تحتاج هامشاً أطول
|
||||
-- من رحلة داخل الحي، والراكب لا يُطلب منه ضبطها — نشتقها من المسافة.
|
||||
`lead_minutes` SMALLINT UNSIGNED NOT NULL DEFAULT 15,
|
||||
|
||||
`status` VARCHAR(20) NOT NULL DEFAULT 'scheduled'
|
||||
COMMENT 'scheduled = بانتظار موعده | dispatching = قفل مؤقت أثناء الإنشاء (يمنع الإطلاق المزدوج) | dispatched = أُنشئت رحلته | cancelled | expired = فات موعده بلا إسناد',
|
||||
|
||||
-- الرحلة الفعلية التي وُلدت من هذا الحجز.
|
||||
`ride_id` VARCHAR(20) NULL DEFAULT NULL,
|
||||
|
||||
-- أختام التذكيرات: يُكتب الختم مرة واحدة فيمنع تكرار الإشعار على كل
|
||||
-- مرور للكرون. راكب يصله «رحلتك بعد ساعة» ستين مرة يُلغي الحجز ويحذف
|
||||
-- التطبيق.
|
||||
`reminded_60_at` DATETIME NULL DEFAULT NULL,
|
||||
`reminded_15_at` DATETIME NULL DEFAULT NULL,
|
||||
|
||||
`note` VARCHAR(255) NULL DEFAULT NULL,
|
||||
`cancelled_reason` VARCHAR(255) NULL DEFAULT NULL,
|
||||
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
-- المفتاح الذي يعتمد عليه الكرون: يمسح المستحق دون قراءة الجدول كله.
|
||||
KEY `idx_due` (`status`, `scheduled_at`),
|
||||
KEY `idx_passenger` (`passenger_id`, `status`),
|
||||
KEY `idx_ride` (`ride_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
|
||||
COMMENT='الحجوزات المسبقة — ومصدر توقّع الطلب المؤكد';
|
||||
|
||||
-- للتراجع:
|
||||
-- DROP TABLE `scheduled_rides`;
|
||||
@@ -0,0 +1,80 @@
|
||||
-- ============================================================
|
||||
-- طلب رحلة بلا إنترنت — عبر SMS (بند 3.1 من دراسة الفرص)
|
||||
--
|
||||
-- المسار: الراكب يرسل رسالة نصية لرقمنا الثابت ← تطبيق أندرويد على
|
||||
-- ذلك الهاتف يوجّهها إلى webhook_sms/webhook.php ← نحلّلها وننشئ رحلة
|
||||
-- ← نرد عليه برسالة، وحين يقبل سائق نرد برسالة ثانية فيها اسمه ولوحته.
|
||||
--
|
||||
-- لا مشغّل اتصالات ولا ترخيص ولا رقم قصير — شريحة عادية في هاتف عادي.
|
||||
-- ============================================================
|
||||
|
||||
-- ── ١) الوارد الخام ─────────────────────────────────────────
|
||||
-- نحفظ كل رسالة تصل كما وصلت، قبل أي تحليل. السبب: المحلّل سيخطئ في
|
||||
-- البداية حتماً (اللهجات، الأخطاء الإملائية، صيغ لم نتوقّعها)، وبلا
|
||||
-- الخام لن نعرف *لماذا* أخطأ ولا كيف نحسّنه.
|
||||
CREATE TABLE IF NOT EXISTS `sms_inbox` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`sender` VARCHAR(32) NOT NULL COMMENT 'رقم المرسل كما وصل من الهاتف',
|
||||
`sender_norm` VARCHAR(32) NOT NULL COMMENT 'الرقم بعد التوحيد (بلا رموز، بمقدمة دولية)',
|
||||
`body` TEXT NOT NULL,
|
||||
`intent` ENUM('ride','payment','unknown') NOT NULL DEFAULT 'unknown',
|
||||
`received_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_sender_norm` (`sender_norm`),
|
||||
KEY `idx_received` (`received_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── ٢) طلبات الرحلة المستخرجة ───────────────────────────────
|
||||
-- جدول وسيط بين الرسالة والرحلة، لا اختصار له. وظيفته الأمنية أساساً:
|
||||
-- add_ride.php يفتح مساراً داخلياً بمفتاح S2S لهذه الطلبات، وشرط المرور
|
||||
-- وجود صف هنا بحالة 'dispatching' يطابق الراكب ونقطة الانطلاق — تماماً
|
||||
-- كما فعلنا مع الحجز المسبق. مفتاح مسرّب وحده لا يخلق رحلة.
|
||||
CREATE TABLE IF NOT EXISTS `sms_ride_requests` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`inbox_id` INT DEFAULT NULL,
|
||||
`sender_norm` VARCHAR(32) NOT NULL,
|
||||
`passenger_id` VARCHAR(100) DEFAULT NULL COMMENT 'يُملأ بعد إيجاد/إنشاء الحساب',
|
||||
`raw_start` VARCHAR(255) DEFAULT NULL COMMENT 'النص كما كتبه الراكب',
|
||||
`raw_end` VARCHAR(255) DEFAULT NULL,
|
||||
`start_location` VARCHAR(100) DEFAULT NULL COMMENT '"lat,lng"',
|
||||
`end_location` VARCHAR(100) DEFAULT NULL,
|
||||
`start_name` VARCHAR(255) DEFAULT NULL COMMENT 'اسم المعلم المطابَق',
|
||||
`end_name` VARCHAR(255) DEFAULT NULL,
|
||||
`distance` DOUBLE DEFAULT 0,
|
||||
`duration` INT DEFAULT 0,
|
||||
`status` ENUM('pending','dispatching','dispatched','failed','unparsed')
|
||||
NOT NULL DEFAULT 'pending',
|
||||
`ride_id` VARCHAR(100) DEFAULT NULL,
|
||||
`attempts` TINYINT NOT NULL DEFAULT 0,
|
||||
`fail_reason` VARCHAR(255) DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_status_created` (`status`, `created_at`),
|
||||
KEY `idx_ride` (`ride_id`),
|
||||
KEY `idx_sender` (`sender_norm`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ── ٣) الصادر ───────────────────────────────────────────────
|
||||
-- طابور لا إرسال مباشر. مزوّدونا الحاليون (Kazumi / Nabeh / Intaleq)
|
||||
-- مسارات OTP لا رسائل حرة، فالمُرسِل اليوم هو نفس الهاتف المستقبِل:
|
||||
-- يسحب من هنا كل بضع ثوانٍ ويرسل عبر شريحته. الطابور يجعل استبدال
|
||||
-- المُرسِل لاحقاً (مشغّل، أو بوابة تجارية) تغييراً في طرف واحد.
|
||||
CREATE TABLE IF NOT EXISTS `sms_outbox` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`recipient` VARCHAR(32) NOT NULL,
|
||||
`body` TEXT NOT NULL,
|
||||
`context` VARCHAR(64) DEFAULT NULL COMMENT 'ride_created / driver_found / error ...',
|
||||
`ref_id` VARCHAR(100) DEFAULT NULL COMMENT 'رقم الرحلة أو الطلب المرتبط',
|
||||
`status` ENUM('pending','sending','sent','failed') NOT NULL DEFAULT 'pending',
|
||||
`attempts` TINYINT NOT NULL DEFAULT 0,
|
||||
`claimed_by` VARCHAR(64) DEFAULT NULL COMMENT 'معرّف الهاتف الذي سحب الرسالة',
|
||||
`claimed_at` DATETIME DEFAULT NULL,
|
||||
`sent_at` DATETIME DEFAULT NULL,
|
||||
`last_error` VARCHAR(255) DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
-- الفهرس على (status, id) هو ما يجعل السحب رخيصاً مهما كبر الجدول.
|
||||
KEY `idx_status_id` (`status`, `id`),
|
||||
KEY `idx_claimed` (`status`, `claimed_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,49 @@
|
||||
-- 2026_08_07_verify_all.sql — فحص حالة كل ترحيلات اليوم
|
||||
--
|
||||
-- قراءة خالصة لا تُعدّل شيئاً. شغّله على أي قاعدة لتعرف ما نُفِّذ وما لا.
|
||||
-- العمود ok: 1 = موجود، 0 = ناقص.
|
||||
--
|
||||
-- sirodb jorSiroDB < backend/migrations/2026_08_07_verify_all.sql
|
||||
-- sirodb 'intaleq-ridesDB' < backend/migrations/2026_08_07_verify_all.sql
|
||||
|
||||
SELECT 'kazan.cancelCompensationCap' AS item,
|
||||
COUNT(*) AS ok FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='kazan' AND COLUMN_NAME='cancelCompensationCap'
|
||||
UNION ALL SELECT 'kazan.cancelMinWaitMinutes', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='kazan' AND COLUMN_NAME='cancelMinWaitMinutes'
|
||||
UNION ALL SELECT 'kazan.passengerCancelCap', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='kazan' AND COLUMN_NAME='passengerCancelCap'
|
||||
UNION ALL SELECT 'kazan.passengerCancelFreeMinutes', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='kazan' AND COLUMN_NAME='passengerCancelFreeMinutes'
|
||||
UNION ALL SELECT 'kazan.driverLateExemptMinutes', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='kazan' AND COLUMN_NAME='driverLateExemptMinutes'
|
||||
|
||||
UNION ALL SELECT 'ride.driverArrivedAt', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ride' AND COLUMN_NAME='driverArrivedAt'
|
||||
UNION ALL SELECT 'ride.pickupApproachKm', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ride' AND COLUMN_NAME='pickupApproachKm'
|
||||
|
||||
UNION ALL SELECT 'driver.blockedUntil', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='driver' AND COLUMN_NAME='blockedUntil'
|
||||
UNION ALL SELECT 'driver.safetyHoldIncidentId', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='driver' AND COLUMN_NAME='safetyHoldIncidentId'
|
||||
|
||||
UNION ALL SELECT 'passengers.noShowCount', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='passengers' AND COLUMN_NAME='noShowCount'
|
||||
|
||||
UNION ALL SELECT 'complaint.first_response_at', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='complaint' AND COLUMN_NAME='first_response_at'
|
||||
UNION ALL SELECT 'complaint.assigned_to', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='complaint' AND COLUMN_NAME='assigned_to'
|
||||
UNION ALL SELECT 'complaint.reason_code', COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='complaint' AND COLUMN_NAME='reason_code'
|
||||
|
||||
UNION ALL SELECT 'driver_orders.idx_order', COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='driver_orders' AND INDEX_NAME='idx_order'
|
||||
|
||||
UNION ALL SELECT 'TABLE driver_leak_events', COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='driver_leak_events'
|
||||
UNION ALL SELECT 'TABLE safety_incidents', COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='safety_incidents'
|
||||
UNION ALL SELECT 'TABLE complaint_compensations', COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='complaint_compensations';
|
||||
@@ -0,0 +1,239 @@
|
||||
-- ============================================================
|
||||
-- محرك الالتزامات العام — بند 3.4 من دراسة الفرص (اقتصاد السائق)
|
||||
--
|
||||
-- سبب وجود هذا الملف ليس التوسّع المستقبلي، بل ثغرة قائمة في الإنتاج:
|
||||
-- `cron_insurance_premiums.php` يقيّد الأقساط في `insurance_premium_ledger`
|
||||
-- منذ إطلاقه، وتعليقه يقول «تقرأه التسوية» — والتسوية غير موجودة. لا
|
||||
-- مرجع واحد لذلك الجدول في المشروع كله خارج الكرون والـmigration. أي أن
|
||||
-- كل قسط تأمين قُيّد حتى اليوم ما زال `pending` ولم يُحصَّل قرشٌ منه.
|
||||
--
|
||||
-- فالخيار كان: كتابة تسوية خاصة بالتأمين، أو تعميم النموذج مرة واحدة.
|
||||
-- والوقود والصيانة والتمويل — البنود الثلاثة التالية في اقتصاد السائق —
|
||||
-- كلها نفس الشكل: التزام دوري أو مقسّط يُخصم من أرباح السائق. كتابة
|
||||
-- تسوية لكل واحد منها تعني أربع نسخ من أخطر منطق في المنصة: المنطق
|
||||
-- الذي يلمس مال السائق.
|
||||
--
|
||||
-- الفصل المعتمد:
|
||||
-- • مُصدِر استحقاق لكل منتج → يقيّد «على السائق كذا»
|
||||
-- • دفتر موحّد → سجل دائم واحد مهما كان المصدر
|
||||
-- • محرك تسوية واحد → الجهة الوحيدة التي تلمس الرصيد
|
||||
--
|
||||
-- جداول التأمين تبقى كما هي — لا تُحذف ولا تُعدَّل. تُنقل بياناتها هنا
|
||||
-- في نهاية هذا الملف، وتبقى الأصلية شاهداً تاريخياً.
|
||||
-- ============================================================
|
||||
|
||||
-- ── ١) المنتجات ─────────────────────────────────────────────
|
||||
-- يعمّم `insurance_plans`. الفرق الجوهري عن الخطة: `kind` يحدّد سلوك
|
||||
-- الاستحقاق نفسه — الدوري يتكرّر بلا نهاية (تأمين)، والمقسّط له أصل
|
||||
-- محدود ينتهي بسداده (صيانة، تمويل)، والمسحوب يُقيَّد عند السحب لا
|
||||
-- على جدول (وقود).
|
||||
CREATE TABLE IF NOT EXISTS `obligation_products` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`code` VARCHAR(60) NOT NULL COMMENT 'معرّف ثابت يُستعمل في الكود',
|
||||
`kind` ENUM('recurring','installment','drawdown') NOT NULL,
|
||||
`name_ar` VARCHAR(160) NOT NULL,
|
||||
`description_ar` TEXT DEFAULT NULL,
|
||||
|
||||
-- الشريك الخارجي: شركة تأمين، سلسلة محطات، ورشة، أو بنك. عمود نصّي
|
||||
-- لا جدول: لا نعرف بعد ما إذا كانت لهذه الجهات دورة حياة تستحق
|
||||
-- جدولاً، وجدول فارغ الغرض أسوأ من عمود.
|
||||
`partner_name` VARCHAR(160) DEFAULT NULL,
|
||||
|
||||
`billing_cycle` ENUM('daily','monthly','none') NOT NULL DEFAULT 'daily'
|
||||
COMMENT 'none للمنتجات المسحوبة — لا دورة لها',
|
||||
`currency` VARCHAR(10) NOT NULL DEFAULT 'JOD',
|
||||
|
||||
-- ── شروط الأهلية ──
|
||||
-- تُخزَّن مع المنتج لا في الكود: الشريك سيغيّرها، والسوق المصري يختلف
|
||||
-- عن الأردني، وتغيير رقم في صف أرخص من نشر إصدار. منقولة كما هي من
|
||||
-- `insurance_plans` لأن المنطق ذاته ينطبق على الوقود والصيانة: كلاهما
|
||||
-- ائتمان يُمنح لسائق قد يختفي.
|
||||
`min_completed_rides` INT NOT NULL DEFAULT 200,
|
||||
`min_rating` DECIMAL(3,2) NOT NULL DEFAULT 4.50,
|
||||
`min_account_days` INT NOT NULL DEFAULT 30,
|
||||
|
||||
-- سقف الخصم اليومي كنسبة من أرباح اليوم. على مستوى المنتج لا النظام:
|
||||
-- قسط التأمين الصغير يحتمل نسبة أعلى من قرض صيانة كبير، والسقف الموحّد
|
||||
-- يعني إمّا خنق السائق أو إبطاء التحصيل.
|
||||
`daily_cap_percent` DECIMAL(5,2) NOT NULL DEFAULT 25.00,
|
||||
|
||||
-- ترتيب المزاحمة حين تستحق التزامات عدة في يوم واحد. الأصغر أولاً:
|
||||
-- التأمين (١٠) قبل الوقود (٢٠) قبل الصيانة (٣٠) — انقطاع التأمين
|
||||
-- يُلغي وثيقةً ويفقد السائق تغطيته، بينما تأخّر قسط صيانة يوماً لا
|
||||
-- يكلّف أحداً شيئاً.
|
||||
`priority` SMALLINT NOT NULL DEFAULT 50,
|
||||
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_code` (`code`),
|
||||
KEY `idx_kind_active` (`kind`, `is_active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
-- ── ٢) التزامات السائقين ────────────────────────────────────
|
||||
-- يعمّم `driver_insurance_policies`: الاشتراك/العقد النشط بين سائق ومنتج.
|
||||
CREATE TABLE IF NOT EXISTS `driver_obligations` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`driver_id` VARCHAR(100) NOT NULL,
|
||||
`product_id` INT NOT NULL,
|
||||
`external_ref` VARCHAR(120) DEFAULT NULL COMMENT 'رقم الوثيقة/العقد لدى الشريك',
|
||||
`status` ENUM('active','suspended','completed','cancelled')
|
||||
NOT NULL DEFAULT 'active',
|
||||
`started_at` DATE NOT NULL,
|
||||
`ended_at` DATE DEFAULT NULL,
|
||||
|
||||
-- مبلغ الدورة الواحدة. منسوخ من المنتج لحظة الاشتراك لا مقروءاً منه:
|
||||
-- رفع سعر الخطة غداً يجب ألّا يغيّر قسط من اشترك أمس بأثر رجعي.
|
||||
`cycle_amount` DECIMAL(12,3) NOT NULL DEFAULT 0,
|
||||
|
||||
-- للمقسّط فقط: الأصل وما سُدِّد منه. المنتج الدوري يتركهما صفراً —
|
||||
-- لا نهاية له فلا معنى لأصلٍ ينفد.
|
||||
`principal_amount` DECIMAL(12,3) NOT NULL DEFAULT 0,
|
||||
`principal_paid` DECIMAL(12,3) NOT NULL DEFAULT 0,
|
||||
|
||||
-- لقطة الأهلية لحظة الاشتراك. بدونها لا يمكن الإجابة لاحقاً على
|
||||
-- «لماذا مُنح هذا السائق ائتماناً؟» حين ينخفض تقييمه أو تتغيّر الشروط.
|
||||
`rides_at_signup` INT NOT NULL DEFAULT 0,
|
||||
`rating_at_signup` DECIMAL(3,2) NOT NULL DEFAULT 0,
|
||||
|
||||
-- آخر يوم قُيّد عنه استحقاق. هذا العمود — لا التاريخ الحالي — هو
|
||||
-- الفلتر الرخيص ضد الاحتساب المزدوج. الحارس الحقيقي هو المفتاح
|
||||
-- الفريد في الدفتر أدناه.
|
||||
`last_charged_on` DATE DEFAULT NULL,
|
||||
`cancel_reason` VARCHAR(255) DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_driver_status` (`driver_id`, `status`),
|
||||
KEY `idx_charge_sweep` (`status`, `last_charged_on`),
|
||||
CONSTRAINT `fk_obligation_product` FOREIGN KEY (`product_id`)
|
||||
REFERENCES `obligation_products` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
-- ── ٣) الدفتر الموحّد ───────────────────────────────────────
|
||||
-- يعمّم `insurance_premium_ledger` بفارق واحد جوهري: `amount_collected`
|
||||
-- و`amount_remaining`. القسط يُدفع كاملاً أو لا يُدفع، أما الوقود
|
||||
-- والصيانة فتحصيلهما جزئي بطبعه — سقف الخصم اليومي يعني أن قيداً بقيمة
|
||||
-- عشرة قد يُحصَّل على ثلاثة أيام. بلا هذين العمودين لا يمكن تمثيل ذلك
|
||||
-- إلا بتفتيت القيد، فيضيع أثر الاستحقاق الأصلي.
|
||||
CREATE TABLE IF NOT EXISTS `obligation_ledger` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`obligation_id` INT NOT NULL,
|
||||
`driver_id` VARCHAR(100) NOT NULL,
|
||||
`product_code` VARCHAR(60) NOT NULL COMMENT 'منسوخ للاستعلام بلا JOIN',
|
||||
`charge_date` DATE NOT NULL COMMENT 'اليوم أو أول الشهر المحتسَب',
|
||||
|
||||
`amount` DECIMAL(12,3) NOT NULL COMMENT 'أصل الاستحقاق',
|
||||
`amount_collected` DECIMAL(12,3) NOT NULL DEFAULT 0,
|
||||
`amount_remaining` DECIMAL(12,3) NOT NULL COMMENT 'amount - amount_collected',
|
||||
`currency` VARCHAR(10) NOT NULL DEFAULT 'JOD',
|
||||
|
||||
-- partial ليست حالة عابرة بل مستقرّة: قيد حُصِّل بعضه ينتظر يوماً
|
||||
-- أفضل. waived للإعفاء الإداري — يُغلق القيد بلا مال، ويبقى أثره.
|
||||
`status` ENUM('pending','partial','settled','waived','failed')
|
||||
NOT NULL DEFAULT 'pending',
|
||||
|
||||
`attempts` SMALLINT NOT NULL DEFAULT 0,
|
||||
`last_attempt_on` DATE DEFAULT NULL,
|
||||
`settled_at` DATETIME DEFAULT NULL,
|
||||
`note` VARCHAR(255) DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
|
||||
-- الحارس الحقيقي ضد الاحتساب المزدوج: استحقاق واحد لكل التزام في
|
||||
-- اليوم الواحد، مهما تكرّر تشغيل الكرون أو تزامنت نسختان منه.
|
||||
UNIQUE KEY `uq_obligation_date` (`obligation_id`, `charge_date`),
|
||||
|
||||
-- فهرس مسح التسوية: تمرّ على المعلّق والجزئي مرتّباً بأولوية المنتج.
|
||||
KEY `idx_settlement_sweep` (`status`, `driver_id`),
|
||||
KEY `idx_driver_date` (`driver_id`, `charge_date`),
|
||||
CONSTRAINT `fk_ledger_obligation` FOREIGN KEY (`obligation_id`)
|
||||
REFERENCES `driver_obligations` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- ترحيل التأمين إلى النموذج العام
|
||||
--
|
||||
-- يعمل هذا القسم أكثر من مرة بلا ضرر: كل إدراج مشروط بعدم وجود ما
|
||||
-- يقابله. سبب الحرص أن الترحيل يلمس التزامات مالية قائمة، وتشغيلاً
|
||||
-- ثانياً بلا حماية يعني ازدواج كل وثيقة وكل قسط.
|
||||
-- ============================================================
|
||||
|
||||
-- ── خطط التأمين تصير منتجات ──
|
||||
-- `code` يُنسخ كما هو ليبقى المعرّف الثابت واحداً بين النموذجين.
|
||||
-- الأولوية ١٠: التأمين أوّل من يُحصَّل عند المزاحمة.
|
||||
INSERT INTO `obligation_products`
|
||||
(`code`, `kind`, `name_ar`, `description_ar`, `partner_name`, `billing_cycle`,
|
||||
`currency`, `min_completed_rides`, `min_rating`, `min_account_days`,
|
||||
`daily_cap_percent`, `priority`, `is_active`)
|
||||
SELECT
|
||||
pl.`code`, 'recurring', pl.`name_ar`, pl.`description_ar`,
|
||||
pr.`name`, pl.`billing_cycle`, pl.`currency`,
|
||||
pl.`min_completed_rides`, pl.`min_rating`, pl.`min_account_days`,
|
||||
25.00, 10, pl.`is_active`
|
||||
FROM `insurance_plans` pl
|
||||
JOIN `insurance_providers` pr ON pr.`id` = pl.`provider_id`
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `obligation_products` op WHERE op.`code` = pl.`code`
|
||||
);
|
||||
|
||||
-- ── الوثائق تصير التزامات ──
|
||||
-- `cycle_amount` يُنسخ من قسط الخطة لحظة الترحيل — نفس مبدأ تثبيت السعر
|
||||
-- الذي يحكم الاشتراكات الجديدة.
|
||||
INSERT INTO `driver_obligations`
|
||||
(`driver_id`, `product_id`, `external_ref`, `status`, `started_at`, `ended_at`,
|
||||
`cycle_amount`, `rides_at_signup`, `rating_at_signup`, `last_charged_on`,
|
||||
`cancel_reason`, `created_at`)
|
||||
SELECT
|
||||
p.`driver_id`, op.`id`, p.`policy_number`,
|
||||
-- 'expired' في التأمين تقابل 'completed' هنا: النموذج العام لا يعرف
|
||||
-- انتهاء صلاحية، يعرف التزاماً بلغ نهايته.
|
||||
CASE p.`status` WHEN 'expired' THEN 'completed' ELSE p.`status` END,
|
||||
p.`started_at`, p.`ended_at`,
|
||||
pl.`premium`, p.`rides_at_signup`, p.`rating_at_signup`, p.`last_charged_on`,
|
||||
p.`cancel_reason`, p.`created_at`
|
||||
FROM `driver_insurance_policies` p
|
||||
JOIN `insurance_plans` pl ON pl.`id` = p.`plan_id`
|
||||
JOIN `obligation_products` op ON op.`code` = pl.`code`
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `driver_obligations` o
|
||||
WHERE o.`driver_id` = p.`driver_id`
|
||||
AND o.`product_id` = op.`id`
|
||||
AND o.`started_at` = p.`started_at`
|
||||
);
|
||||
|
||||
-- ── الأقساط المعلّقة تصير قيوداً ──
|
||||
-- هذه هي الغاية العملية من الترحيل كله: هذه الصفوف — أقساط حقيقية
|
||||
-- تراكمت في الإنتاج بلا تحصيل — تصير مرئية لمحرك التسوية.
|
||||
--
|
||||
-- `settled` القديمة تُنقل أيضاً رغم أنها لن تُحصَّل: دفتر ناقص التاريخ
|
||||
-- لا يُسوّى مع شريك.
|
||||
INSERT INTO `obligation_ledger`
|
||||
(`obligation_id`, `driver_id`, `product_code`, `charge_date`,
|
||||
`amount`, `amount_collected`, `amount_remaining`, `currency`,
|
||||
`status`, `settled_at`, `note`, `created_at`)
|
||||
SELECT
|
||||
o.`id`, l.`driver_id`, op.`code`, l.`charge_date`,
|
||||
l.`amount`,
|
||||
CASE WHEN l.`status` = 'settled' THEN l.`amount` ELSE 0 END,
|
||||
CASE WHEN l.`status` = 'settled' THEN 0 ELSE l.`amount` END,
|
||||
l.`currency`,
|
||||
-- 'failed' القديمة تعود 'pending': الفشل السابق لم يكن قراراً بل
|
||||
-- غياب محرك. حجبها عن التسوية الآن يعني إسقاط مال مستحق فعلاً.
|
||||
CASE l.`status` WHEN 'failed' THEN 'pending' ELSE l.`status` END,
|
||||
l.`settled_at`, l.`note`, l.`created_at`
|
||||
FROM `insurance_premium_ledger` l
|
||||
JOIN `driver_insurance_policies` p ON p.`id` = l.`policy_id`
|
||||
JOIN `insurance_plans` pl ON pl.`id` = p.`plan_id`
|
||||
JOIN `obligation_products` op ON op.`code` = pl.`code`
|
||||
JOIN `driver_obligations` o ON o.`driver_id` = p.`driver_id`
|
||||
AND o.`product_id` = op.`id`
|
||||
AND o.`started_at` = p.`started_at`
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `obligation_ledger` ol
|
||||
WHERE ol.`obligation_id` = o.`id` AND ol.`charge_date` = l.`charge_date`
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
-- ============================================================
|
||||
-- بوابة الائتمان: لا التزام إلا لمن تمرّ أرباحه بالمحفظة
|
||||
--
|
||||
-- المشكلة التي يعالجها هذا الملف (قرار المالك 2026-08-09):
|
||||
--
|
||||
-- سقف الخصم اليومي في محرك الالتزامات نسبة من أرباح اليوم، وأرباح اليوم
|
||||
-- تُقاس من الصفوف الموجبة في `driverWallet`. سائق الكاش الذي يحصّل نقداً
|
||||
-- ولا تمرّ أرباحه بالمحفظة يظهر بأرباح صفر كل يوم — فسقفه صفر، فلا
|
||||
-- يُحصَّل منه شيء أبداً.
|
||||
--
|
||||
-- هذا محتمَل في التأمين (قسط صغير متكرر، والخسارة محدودة ومرئية). وهو
|
||||
-- غير محتمَل إطلاقاً في الوقود والصيانة والتمويل: هناك تسلّم السائق
|
||||
-- قيمةً فعلية — وقوداً في خزّان سيارته أو إصلاحاً في ورشة — مقابل وعد
|
||||
-- بالسداد من قناة لا يمرّ بها ماله. هذا ليس ائتماناً بل هبة.
|
||||
--
|
||||
-- الحل: شرط أهلية إضافي مخزَّن مع المنتج لا في الكود، على غرار شروط
|
||||
-- الرحلات والتقييم القائمة. العتبة تختلف بطبيعة المنتج: قسط تأمين يومي
|
||||
-- صغير لا يحتاج ما يحتاجه قرض صيانة.
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE `obligation_products`
|
||||
-- المفتاح العام. تركه قابلاً للإطفاء ليس تساهلاً: منتج مستقبلي قد
|
||||
-- يُدفع مقدَّماً أو يموّله طرف ثالث، فلا معنى لبوابة ائتمان عليه.
|
||||
ADD COLUMN `requires_wallet_income` TINYINT(1) NOT NULL DEFAULT 1
|
||||
COMMENT 'يشترط أن تمرّ أرباح السائق بالمحفظة' AFTER `min_account_days`,
|
||||
|
||||
-- عدد الأيام المتمايزة التي دخل فيها مالٌ محفظةَ السائق خلال آخر
|
||||
-- ثلاثين يوماً. المقياس أيام لا مبلغ لأن السؤال هنا ليس «كم يكسب؟»
|
||||
-- بل «هل قناة التحصيل تعمل؟» — سائق دخله مرة واحدة كبيرة قد يكون
|
||||
-- استرداداً أو حادثة، وسائق دخله اثنا عشر يوماً متفرّقاً قناته حيّة.
|
||||
ADD COLUMN `min_wallet_days_30d` SMALLINT NOT NULL DEFAULT 10
|
||||
COMMENT 'أيام دخل في المحفظة خلال آخر 30 يوماً' AFTER `requires_wallet_income`,
|
||||
|
||||
-- الحد الأدنى للدخل المار بالمحفظة في آخر ثلاثين يوماً. يُقاس بعملة
|
||||
-- المنتج. صفر يعني «لا شرط مبلغ» ويُكتفى بشرط الأيام.
|
||||
ADD COLUMN `min_wallet_income_30d` DECIMAL(12,3) NOT NULL DEFAULT 0
|
||||
COMMENT 'إجمالي الدخل المار بالمحفظة خلال آخر 30 يوماً' AFTER `min_wallet_days_30d`;
|
||||
|
||||
|
||||
-- ── عتبة التأمين ────────────────────────────────────────────
|
||||
-- منتجات التأمين المرحَّلة تخضع للبوابة أيضاً — القسط الذي تتحمّله
|
||||
-- الشركة ثم تسترده من الأرباح ائتمانٌ بالمعنى نفسه.
|
||||
--
|
||||
-- لكن العتبة متساهلة عمداً (ثمانية أيام، بلا شرط مبلغ): التأمين مكافأة
|
||||
-- استمرار مشروطة أصلاً بمئتي رحلة وتقييم جيد، وتشديد بوابة ثانية فوقها
|
||||
-- يفرغ المنتج من غرضه.
|
||||
--
|
||||
-- هذا يمسّ الاشتراكات **الجديدة** وحدها. الالتزامات القائمة لا تُفحص
|
||||
-- ثانيةً: سحب تغطية من سائق مؤمَّن اليوم بسبب شرط سُنّ اليوم عقوبة
|
||||
-- بأثر رجعي.
|
||||
UPDATE `obligation_products` op
|
||||
JOIN `insurance_plans` pl ON pl.`code` = op.`code`
|
||||
SET op.`requires_wallet_income` = 1,
|
||||
op.`min_wallet_days_30d` = 8,
|
||||
op.`min_wallet_income_30d` = 0;
|
||||
@@ -0,0 +1,145 @@
|
||||
-- ============================================================
|
||||
-- محفظة الوقود — بند 3.4 من دراسة الفرص، الخطوة الثالثة
|
||||
--
|
||||
-- الفكرة: اتفاق مع سلسلة محطات، السائق يتزوّد وقوداً بلا نقد، والقيمة
|
||||
-- تُقيَّد ديناً عليه يُخصم من أرباحه بالسقف اليومي نفسه. قيمة فورية
|
||||
-- ملموسة للسائق — الوقود مشكلته اليومية الأولى — وأقوى أثراً من أي
|
||||
-- زيادة عمولة، لأنها تحلّ نقص السيولة لا تزيد الرقم.
|
||||
--
|
||||
-- هذا أول منتج من نوع `drawdown`: لا دورة فوترة له. الاستحقاق يُقيَّد
|
||||
-- لحظة السحب من المحطة لا على جدول زمني.
|
||||
-- ============================================================
|
||||
|
||||
-- ── ١) تعدّد الاستحقاقات في اليوم الواحد ────────────────────
|
||||
--
|
||||
-- المفتاح الفريد الحالي (obligation_id, charge_date) يمنع قيدين لالتزام
|
||||
-- واحد في يوم واحد — وهو بالضبط ما نريده للتأمين: قسط واحد في اليوم.
|
||||
--
|
||||
-- لكنه يكسر الوقود: سائق يتزوّد مرتين في يوم واحد سيصطدم قيده الثاني
|
||||
-- بالمفتاح فيُرفض، فيحصل على وقود بلا دَين يقابله. اكتُشف هذا عند
|
||||
-- تصميم المُصدِر لا بعد تشغيله.
|
||||
--
|
||||
-- الحل: `source_ref` يميّز السحبة. فارغاً للمنتجات الدورية — فيبقى
|
||||
-- ضمانها كما هو حرفياً — ومملوءاً برقم السحبة للوقود.
|
||||
--
|
||||
-- سلسلة فارغة لا NULL: MySQL يعتبر كل NULL مختلفاً عن غيره في المفاتيح
|
||||
-- الفريدة، فعمود NULL كان سيلغي الحماية عن التأمين بصمت.
|
||||
ALTER TABLE `obligation_ledger`
|
||||
ADD COLUMN `source_ref` VARCHAR(80) NOT NULL DEFAULT ''
|
||||
COMMENT 'مرجع الحدث المُنشئ — فارغ للدوري، رقم السحبة للوقود'
|
||||
AFTER `charge_date`;
|
||||
|
||||
ALTER TABLE `obligation_ledger`
|
||||
DROP INDEX `uq_obligation_date`,
|
||||
ADD UNIQUE KEY `uq_obligation_event` (`obligation_id`, `charge_date`, `source_ref`);
|
||||
|
||||
|
||||
-- ── ٢) سقف الائتمان ─────────────────────────────────────────
|
||||
-- `principal_amount` لا يصلح هنا: هو أصلٌ ينفد بالسداد (قرض صيانة)،
|
||||
-- بينما سقف الوقود متجدّد — يُسدَّد فيعود متاحاً، كبطاقة ائتمان.
|
||||
ALTER TABLE `driver_obligations`
|
||||
ADD COLUMN `credit_limit` DECIMAL(12,3) NOT NULL DEFAULT 0
|
||||
COMMENT 'سقف متجدّد لمنتجات السحب (الوقود)' AFTER `principal_amount`;
|
||||
|
||||
|
||||
-- ── ٣) المحطات الشريكة ──────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS `fuel_stations` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`name_ar` VARCHAR(160) NOT NULL,
|
||||
`chain_name` VARCHAR(120) DEFAULT NULL COMMENT 'اسم السلسلة إن كانت فرعاً',
|
||||
`city` VARCHAR(80) DEFAULT NULL,
|
||||
`address` VARCHAR(255) DEFAULT NULL,
|
||||
`lat` DECIMAL(10,7) DEFAULT NULL,
|
||||
`lng` DECIMAL(10,7) DEFAULT NULL,
|
||||
`phone` VARCHAR(40) DEFAULT NULL,
|
||||
|
||||
-- مفتاح المحطة مخزَّن مُهشَّراً لا نصّاً. المحطات أطراف خارجية وأجهزتها
|
||||
-- خارج سيطرتنا، وتسريب قاعدة البيانات يجب ألّا يسلّم مفاتيح تشغيلية
|
||||
-- لأحد. المفتاح الأصلي يُعطى للمحطة مرة واحدة عند الإنشاء.
|
||||
`api_key_hash` CHAR(64) NOT NULL COMMENT 'sha256 لمفتاح المحطة',
|
||||
|
||||
-- خصم الشريك عن سعر المضخة. يُسجَّل مع كل سحبة لأنه يتغيّر بالتفاوض،
|
||||
-- وحسابه لاحقاً بالنسبة الحالية يزوّر تسويات الماضي.
|
||||
`discount_percent` DECIMAL(5,2) NOT NULL DEFAULT 0,
|
||||
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_active_city` (`is_active`, `city`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
-- ── ٤) قسائم السحب ──────────────────────────────────────────
|
||||
-- دورة الحياة: issued → redeemed | expired | cancelled
|
||||
--
|
||||
-- لماذا قسيمة مسبقة لا خصم مباشر عند المضخة؟ لأن عامل المحطة لا يملك
|
||||
-- وسيلة للتحقق من هوية السائق ولا من سقفه. القسيمة تنقل القرار إلى
|
||||
-- تطبيق السائق (حيث الهوية مؤكَّدة والسقف معلوم)، ولا يبقى على المحطة
|
||||
-- إلا تأكيد رقم.
|
||||
CREATE TABLE IF NOT EXISTS `fuel_vouchers` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`obligation_id` INT NOT NULL,
|
||||
`driver_id` VARCHAR(100) NOT NULL,
|
||||
`station_id` INT DEFAULT NULL COMMENT 'يُملأ عند الصرف',
|
||||
|
||||
`code` VARCHAR(12) NOT NULL COMMENT 'الرقم الذي يعطيه السائق للمحطة',
|
||||
|
||||
-- المبلغ المحجوز عند الإصدار، والمصروف فعلاً عند التأكيد. الفرق
|
||||
-- طبيعي: السائق يطلب عشرين ويتزوّد بخمسة عشر. الدَّين على المصروف.
|
||||
`amount_authorized` DECIMAL(12,3) NOT NULL,
|
||||
`amount_redeemed` DECIMAL(12,3) NOT NULL DEFAULT 0,
|
||||
`discount_percent` DECIMAL(5,2) NOT NULL DEFAULT 0,
|
||||
`currency` VARCHAR(10) NOT NULL DEFAULT 'JOD',
|
||||
|
||||
`status` ENUM('issued','redeemed','expired','cancelled')
|
||||
NOT NULL DEFAULT 'issued',
|
||||
|
||||
-- صلاحية قصيرة. قسيمة مفتوحة إلى الأبد تحجز من سقف السائق مبلغاً
|
||||
-- لن يُستعمل، فيجد نفسه ممنوعاً من التزوّد بسبب قسيمة نسيها.
|
||||
`expires_at` DATETIME NOT NULL,
|
||||
`redeemed_at` DATETIME DEFAULT NULL,
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
|
||||
-- الرمز فريد مطلقاً: عامل المحطة يُدخل رقماً بلا سياق، فتكراره ولو
|
||||
-- بين سائقين مختلفين يعني صرفاً على حساب الشخص الخطأ.
|
||||
UNIQUE KEY `uq_code` (`code`),
|
||||
KEY `idx_driver_status` (`driver_id`, `status`),
|
||||
KEY `idx_open_vouchers` (`status`, `expires_at`),
|
||||
CONSTRAINT `fk_voucher_obligation` FOREIGN KEY (`obligation_id`)
|
||||
REFERENCES `driver_obligations` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_voucher_station` FOREIGN KEY (`station_id`)
|
||||
REFERENCES `fuel_stations` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
-- ── ٥) منتج الوقود ──────────────────────────────────────────
|
||||
-- سقف الخصم اليومي ١٥٪ لا ٢٥٪ كالتأمين: دَين الوقود أكبر من قسط يومي،
|
||||
-- واقتطاع ربع الدخل لسداده يترك السائق بلا ما يكفي ليعمل غداً — فيعود
|
||||
-- محتاجاً وقوداً بالدَّين، وتلك حلقة لا تُغلق.
|
||||
--
|
||||
-- بوابة الائتمان أشدّ من التأمين (١٢ يوماً مقابل ٨) لأن هنا قيمةً فعلية
|
||||
-- تُسلَّم في خزّان السيارة، لا تغطيةً احتمالية.
|
||||
--
|
||||
-- **يُنشأ مُطفأً (`is_active = 0`) عمداً.** قرار المالك: تُبنى الشاشات
|
||||
-- وتُجهَّز الأمور، ولا يُشغَّل المنتج إلا بعد إتمام الاتفاق مع سلسلة
|
||||
-- المحطات. المنتج المُطفأ يمنع السائقين من التفعيل والصرف، بينما تعمل
|
||||
-- الشاشة الإدارية بالكامل للتجهيز والعرض.
|
||||
--
|
||||
-- التشغيل بصفّ واحد من الشاشة الإدارية (تبديل حالة المنتج) — لا بنشر
|
||||
-- إصدار جديد.
|
||||
INSERT INTO `obligation_products`
|
||||
(`code`, `kind`, `name_ar`, `description_ar`, `partner_name`, `billing_cycle`,
|
||||
`currency`, `min_completed_rides`, `min_rating`, `min_account_days`,
|
||||
`requires_wallet_income`, `min_wallet_days_30d`, `min_wallet_income_30d`,
|
||||
`daily_cap_percent`, `priority`, `is_active`)
|
||||
SELECT
|
||||
'fuel_wallet', 'drawdown', 'محفظة الوقود',
|
||||
'تزوّد بالوقود من المحطات الشريكة بلا نقد، والقيمة تُخصم من أرباحك تدريجياً بسقف يومي.',
|
||||
NULL, 'none', 'JOD',
|
||||
200, 4.50, 30,
|
||||
1, 12, 0,
|
||||
15.00, 20, 0
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `obligation_products` WHERE `code` = 'fuel_wallet'
|
||||
);
|
||||
@@ -0,0 +1,75 @@
|
||||
-- _helpers.sql — إجراءات ترحيل قابلة لإعادة التشغيل
|
||||
--
|
||||
-- المشكلة: ALTER TABLE ... ADD COLUMN في MySQL يُجهض **كل** الجملة عند
|
||||
-- أول عمود مكرّر. فإعادة تشغيل ترحيل نُفِّذ جزئياً تفشل، وإعادة تشغيل
|
||||
-- ترحيل نُفِّذ كاملاً تفشل أيضاً — بلا أن تعرف أيّهما.
|
||||
--
|
||||
-- هذا لدغ نشرَ سيرو ثلاث مرات في يوم واحد، ويزداد سوءاً مع تعدد النسخ
|
||||
-- (intaleq / tripz / siro) حيث لا أحد يتذكّر ما نُفِّذ أين.
|
||||
--
|
||||
-- الحل: إجراءات تفحص information_schema أولاً فتتخطّى الموجود بصمت.
|
||||
--
|
||||
-- شغّل هذا الملف مرة واحدة على كل قاعدة قبل أي ترحيل يستخدمه.
|
||||
|
||||
DELIMITER //
|
||||
|
||||
DROP PROCEDURE IF EXISTS `siro_add_column` //
|
||||
/**
|
||||
* يضيف عموداً إن لم يكن موجوداً. آمن لإعادة التشغيل.
|
||||
*
|
||||
* مثال:
|
||||
* CALL siro_add_column('ride', 'driverArrivedAt',
|
||||
* 'DATETIME NULL DEFAULT NULL COMMENT ''لحظة الوصول''');
|
||||
*/
|
||||
CREATE PROCEDURE `siro_add_column`(
|
||||
IN p_table VARCHAR(64),
|
||||
IN p_column VARCHAR(64),
|
||||
IN p_definition TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = p_table
|
||||
AND COLUMN_NAME = p_column
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN `',
|
||||
p_column, '` ', p_definition);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
SELECT CONCAT('أُضيف: ', p_table, '.', p_column) AS result;
|
||||
ELSE
|
||||
SELECT CONCAT('موجود مسبقاً: ', p_table, '.', p_column) AS result;
|
||||
END IF;
|
||||
END //
|
||||
|
||||
DROP PROCEDURE IF EXISTS `siro_add_index` //
|
||||
/**
|
||||
* يضيف فهرساً إن لم يكن موجوداً.
|
||||
* CALL siro_add_index('driver_orders', 'idx_order', '(`order_id`)');
|
||||
*/
|
||||
CREATE PROCEDURE `siro_add_index`(
|
||||
IN p_table VARCHAR(64),
|
||||
IN p_index VARCHAR(64),
|
||||
IN p_columns TEXT
|
||||
)
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = p_table
|
||||
AND INDEX_NAME = p_index
|
||||
) THEN
|
||||
SET @ddl = CONCAT('ALTER TABLE `', p_table, '` ADD INDEX `',
|
||||
p_index, '` ', p_columns);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
SELECT CONCAT('أُضيف فهرس: ', p_table, '.', p_index) AS result;
|
||||
ELSE
|
||||
SELECT CONCAT('فهرس موجود مسبقاً: ', p_table, '.', p_index) AS result;
|
||||
END IF;
|
||||
END //
|
||||
|
||||
DELIMITER ;
|
||||
Reference in New Issue
Block a user