Files
Siro/backend/migrations/2026_08_10_fuel_wallet.sql
T

146 lines
9.1 KiB
SQL
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================
-- محفظة الوقود — بند 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'
);