114 lines
4.6 KiB
PHP
114 lines
4.6 KiB
PHP
<?php
|
||
/**
|
||
* fuel/functions.php — منطق محفظة الوقود المشترك
|
||
* ─────────────────────────────────────────────────────────────
|
||
* محفظة الوقود منتج `drawdown` في محرك الالتزامات: لا دورة فوترة له،
|
||
* والاستحقاق يُقيَّد لحظة الصرف من المحطة لا على جدول زمني.
|
||
*
|
||
* كل ما يخصّ المال هنا يقتصر على **قيد الدَّين**. الخصم من رصيد السائق
|
||
* يبقى حكراً على محرك التسوية، كما في التأمين.
|
||
*/
|
||
|
||
require_once __DIR__ . '/../obligations/functions.php';
|
||
|
||
const FUEL_PRODUCT_CODE = 'fuel_wallet';
|
||
const FUEL_VOUCHER_TTL_MIN = 45;
|
||
|
||
/**
|
||
* التزام الوقود النشط للسائق، أو null.
|
||
*/
|
||
function fuelObligation(PDO $con, string $driverId): ?array
|
||
{
|
||
$st = $con->prepare("
|
||
SELECT o.*, p.currency, p.daily_cap_percent
|
||
FROM driver_obligations o
|
||
JOIN obligation_products p ON p.id = o.product_id
|
||
WHERE o.driver_id = ? AND p.code = ? AND o.status = 'active'
|
||
LIMIT 1
|
||
");
|
||
$st->execute([$driverId, FUEL_PRODUCT_CODE]);
|
||
|
||
return $st->fetch(PDO::FETCH_ASSOC) ?: null;
|
||
}
|
||
|
||
/**
|
||
* يُنهي صلاحية القسائم التي مضى وقتها.
|
||
*
|
||
* يُستدعى قبل أي حساب سقف لا في كرون: القسيمة المنتهية تحجز من سقف
|
||
* السائق، وتأخير تحريرها ساعةً كاملةً حتى يمرّ الكرون يعني منعه من
|
||
* التزوّد بسبب قسيمة ماتت. التنظيف عند القراءة يجعل الرقم صادقاً دائماً.
|
||
*/
|
||
function fuelExpireStaleVouchers(PDO $con, string $driverId): void
|
||
{
|
||
$con->prepare("
|
||
UPDATE fuel_vouchers
|
||
SET status = 'expired'
|
||
WHERE driver_id = ? AND status = 'issued' AND expires_at < NOW()
|
||
")->execute([$driverId]);
|
||
}
|
||
|
||
/**
|
||
* حالة سقف السائق: المستخدَم، المحجوز، والمتاح.
|
||
*
|
||
* المحجوز يُطرح كالمستخدَم تماماً: قسيمة أُصدرت ولم تُصرف بعد قد تُصرف
|
||
* بعد لحظة، والسماح بإصدار قسائم بمجموع يتجاوز السقف يعني سائقاً
|
||
* يتزوّد بضعف ما يستحق ثم لا نجد ما نخصم منه.
|
||
*
|
||
* @return array{limit: float, outstanding: float, held: float, available: float}
|
||
*/
|
||
function fuelCreditState(PDO $con, array $obligation): array
|
||
{
|
||
$obligationId = (int) $obligation['id'];
|
||
|
||
$st = $con->prepare("
|
||
SELECT COALESCE(SUM(amount_remaining), 0)
|
||
FROM obligation_ledger
|
||
WHERE obligation_id = ? AND status IN ('pending', 'partial')
|
||
");
|
||
$st->execute([$obligationId]);
|
||
$outstanding = (float) $st->fetchColumn();
|
||
|
||
$st = $con->prepare("
|
||
SELECT COALESCE(SUM(amount_authorized), 0)
|
||
FROM fuel_vouchers
|
||
WHERE obligation_id = ? AND status = 'issued' AND expires_at >= NOW()
|
||
");
|
||
$st->execute([$obligationId]);
|
||
$held = (float) $st->fetchColumn();
|
||
|
||
$limit = (float) $obligation['credit_limit'];
|
||
$available = round($limit - $outstanding - $held, 3);
|
||
|
||
return [
|
||
'limit' => $limit,
|
||
'outstanding' => round($outstanding, 3),
|
||
'held' => round($held, 3),
|
||
'available' => max($available, 0.0),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* يولّد رمز قسيمة غير متوقَّع.
|
||
*
|
||
* `random_int` لا `rand`: الرمز مفتاحُ صرفٍ فعلي، ومولّد عشوائية قابل
|
||
* للتنبّؤ يعني أن من يعرف رمزاً واحداً قد يخمّن التالي ويتزوّد على
|
||
* حساب سائق آخر. عشرة أرقام تُبقي المجال واسعاً والرقم قابلاً للإملاء
|
||
* على عامل المحطة هاتفياً.
|
||
*/
|
||
function fuelGenerateCode(PDO $con): string
|
||
{
|
||
for ($i = 0; $i < 10; $i++) {
|
||
$code = (string) random_int(1000000000, 9999999999);
|
||
|
||
$st = $con->prepare("SELECT 1 FROM fuel_vouchers WHERE code = ? LIMIT 1");
|
||
$st->execute([$code]);
|
||
if (!$st->fetchColumn()) {
|
||
return $code;
|
||
}
|
||
}
|
||
|
||
// عشر محاولات فاشلة على مجال من تسعة مليارات ليست صدفة — هي عطل
|
||
// (قاعدة ممتلئة أو مولّد معطوب). الاستمرار هنا يعني رمزاً مكرّراً.
|
||
throw new RuntimeException('تعذّر توليد رمز قسيمة فريد');
|
||
}
|