-- ============================================================ -- محفظة الوقود — بند 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' );