Update: 2026-08-08 12:58:16
This commit is contained in:
@@ -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'
|
||||
);
|
||||
Reference in New Issue
Block a user