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