Update: 2026-08-08 12:58:16
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* fuel/cancel_voucher.php — إلغاء قسيمة لم تُصرف
|
||||
*
|
||||
* يحرّر المبلغ المحجوز فوراً. بدونه ينتظر السائق انتهاء الصلاحية ليعود
|
||||
* سقفه — وهو ينتظر عند مضخة ليتزوّد بمبلغ آخر.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
require_once __DIR__ . '/functions.php';
|
||||
|
||||
$driverId = $user_id ?? '';
|
||||
if (empty($driverId) || ($role ?? '') !== 'driver') {
|
||||
jsonError('Unauthorized', 401);
|
||||
}
|
||||
|
||||
// الشرط على driver_id إلى جانب الحالة: الرمز وحده يكفي للعثور على
|
||||
// قسيمة، ولا يجوز أن يُلغي سائقٌ قسيمة غيره برمز وقع بيده.
|
||||
$st = $con->prepare("
|
||||
UPDATE fuel_vouchers
|
||||
SET status = 'cancelled'
|
||||
WHERE driver_id = ? AND status = 'issued'
|
||||
");
|
||||
$st->execute([$driverId]);
|
||||
|
||||
if ($st->rowCount() === 0) {
|
||||
jsonError('لا توجد قسيمة قابلة للإلغاء');
|
||||
}
|
||||
|
||||
$obligation = fuelObligation($con, $driverId);
|
||||
$state = $obligation ? fuelCreditState($con, $obligation) : null;
|
||||
|
||||
jsonSuccess([
|
||||
'cancelled' => true,
|
||||
'available' => $state['available'] ?? null,
|
||||
], 'أُلغيت القسيمة وعاد المبلغ إلى سقفك');
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* fuel/enroll.php — تفعيل محفظة الوقود للسائق
|
||||
*
|
||||
* يفتح التزام `fuel_wallet` بسقف ائتمان مبدئي. لا يصرف شيئاً — الصرف
|
||||
* يبدأ بقسيمة، والدَّين يُقيَّد عند تأكيد المحطة.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
require_once __DIR__ . '/functions.php';
|
||||
require_once __DIR__ . '/../driver_assurance/eligibility.php';
|
||||
|
||||
$driverId = $user_id ?? '';
|
||||
if (empty($driverId) || ($role ?? '') !== 'driver') {
|
||||
jsonError('Unauthorized', 401);
|
||||
}
|
||||
|
||||
if (fuelObligation($con, $driverId)) {
|
||||
jsonError('محفظة الوقود مفعّلة لديك بالفعل');
|
||||
}
|
||||
|
||||
$product = obligationProduct($con, FUEL_PRODUCT_CODE);
|
||||
if (!$product) {
|
||||
error_log('[fuel/enroll] منتج محفظة الوقود غير موجود أو مُطفأ');
|
||||
jsonError('محفظة الوقود غير متاحة حالياً');
|
||||
}
|
||||
|
||||
// شروط الرحلات والتقييم تُقاس بالدالة نفسها التي يستعملها التأمين:
|
||||
// المنطق واحد (سائق أثبت أنه باقٍ)، وازدواج تطبيقه كان سينتج معيارين
|
||||
// يفترقان بصمت عند أول تعديل.
|
||||
$check = assuranceCheckEligibility($con, $driverId, $product);
|
||||
if (!$check['eligible']) {
|
||||
jsonError('لم تستوفِ شروط محفظة الوقود بعد: ' . implode('، ', $check['reasons']));
|
||||
}
|
||||
|
||||
// بوابة الائتمان أشدّ هنا منها في التأمين: الوقود قيمة فعلية تُسلَّم في
|
||||
// خزّان السيارة، ولا سبيل لاستردادها إلا من أرباح تمرّ بالمحفظة.
|
||||
$gate = obligationCheckWalletIncome($product, $driverId);
|
||||
if (!$gate['eligible']) {
|
||||
jsonError(
|
||||
'محفظة الوقود تُسدَّد من أرباحك في المحفظة، ولذلك تشترط أن تمرّ أرباحك بها: '
|
||||
. implode('، ', $gate['reasons'])
|
||||
);
|
||||
}
|
||||
|
||||
// السقف المبدئي واحد للجميع في هذه المرحلة. رفعه بحسب سلوك السداد قرار
|
||||
// لاحق يحتاج بيانات لا نملكها بعد — ومنح سقف متدرّج بلا تاريخ سداد
|
||||
// تخمينٌ يرتدي ثوب الخوارزمية.
|
||||
$defaultLimit = (float) (getenv('FUEL_DEFAULT_CREDIT_LIMIT') ?: 30);
|
||||
|
||||
try {
|
||||
$con->beginTransaction();
|
||||
|
||||
$obligationId = obligationOpen($con, $driverId, FUEL_PRODUCT_CODE, [
|
||||
'cycle_amount' => 0, // منتج سحب: لا قسط دوري له
|
||||
'rides' => $check['rides'],
|
||||
'rating' => $check['rating'],
|
||||
]);
|
||||
|
||||
$con->prepare("UPDATE driver_obligations SET credit_limit = ? WHERE id = ?")
|
||||
->execute([$defaultLimit, $obligationId]);
|
||||
|
||||
$con->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($con->inTransaction()) $con->rollBack();
|
||||
error_log('[fuel/enroll] ' . $e->getMessage());
|
||||
jsonError('تعذّر تفعيل محفظة الوقود حالياً');
|
||||
}
|
||||
|
||||
error_log("[fuel] فُعّلت محفظة الوقود للسائق $driverId بسقف $defaultLimit");
|
||||
|
||||
jsonSuccess([
|
||||
'obligation_id' => $obligationId,
|
||||
'credit_limit' => $defaultLimit,
|
||||
'available' => $defaultLimit,
|
||||
'currency' => $product['currency'],
|
||||
], 'تم تفعيل محفظة الوقود');
|
||||
@@ -0,0 +1,113 @@
|
||||
<?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('تعذّر توليد رمز قسيمة فريد');
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* fuel/get.php — حالة محفظة الوقود للسائق
|
||||
*
|
||||
* السقف، المستخدَم، المحجوز، والمتاح — ثم آخر الحركات. الأرقام الأربعة
|
||||
* معاً لا المتاح وحده: السائق الذي يرى «المتاح ٥» بلا تفسير يظنّها
|
||||
* عطلاً، والذي يرى «سقفك ٣٠، عليك ٢٠، محجوز ٥» يفهم ويتصرّف.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
require_once __DIR__ . '/functions.php';
|
||||
|
||||
$driverId = $user_id ?? '';
|
||||
if (empty($driverId) || ($role ?? '') !== 'driver') {
|
||||
jsonError('Unauthorized', 401);
|
||||
}
|
||||
|
||||
$obligation = fuelObligation($con, $driverId);
|
||||
|
||||
if (!$obligation) {
|
||||
jsonSuccess([
|
||||
'enrolled' => false,
|
||||
'credit' => null,
|
||||
'voucher' => null,
|
||||
'entries' => [],
|
||||
], 'محفظة الوقود غير مفعّلة');
|
||||
}
|
||||
|
||||
fuelExpireStaleVouchers($con, $driverId);
|
||||
$state = fuelCreditState($con, $obligation);
|
||||
|
||||
$voucher = null;
|
||||
$entries = [];
|
||||
|
||||
try {
|
||||
$st = $con->prepare("
|
||||
SELECT id, code, amount_authorized, currency, expires_at
|
||||
FROM fuel_vouchers
|
||||
WHERE driver_id = ? AND status = 'issued' AND expires_at >= NOW()
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([$driverId]);
|
||||
if ($row = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||
$voucher = [
|
||||
'voucher_id' => (int) $row['id'],
|
||||
'code' => $row['code'],
|
||||
'amount' => (float) $row['amount_authorized'],
|
||||
'expires_at' => $row['expires_at'],
|
||||
];
|
||||
}
|
||||
|
||||
// آخر عشرين حركة: كافية لشهر تزوّد، وتمنع رداً ينمو بلا حد.
|
||||
$st = $con->prepare("
|
||||
SELECT charge_date, amount, amount_collected, amount_remaining,
|
||||
currency, status, note
|
||||
FROM obligation_ledger
|
||||
WHERE obligation_id = ?
|
||||
ORDER BY id DESC LIMIT 20
|
||||
");
|
||||
$st->execute([(int) $obligation['id']]);
|
||||
$entries = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log('[fuel/get] تعذّرت قراءة الحالة: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
jsonSuccess([
|
||||
'enrolled' => true,
|
||||
'credit' => [
|
||||
'limit' => $state['limit'],
|
||||
'outstanding' => $state['outstanding'],
|
||||
'held' => $state['held'],
|
||||
'available' => $state['available'],
|
||||
'currency' => $obligation['currency'],
|
||||
// نسبة الاقتطاع اليومي معروضة صراحةً: الخصم من الأرباح يجب ألّا
|
||||
// يكون مفاجأة، والسائق الذي يعرف أن الاقتطاع ١٥٪ يخطّط عليه.
|
||||
'daily_cap_percent' => (float) $obligation['daily_cap_percent'],
|
||||
],
|
||||
'voucher' => $voucher,
|
||||
'entries' => $entries,
|
||||
], 'success');
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
/**
|
||||
* fuel/redeem.php — تأكيد المحطة صرف القسيمة
|
||||
* ─────────────────────────────────────────────────────────────
|
||||
* هذه هي **نقطة قيد الاستحقاق** لمنتج الوقود: اللحظة التي يتحوّل فيها
|
||||
* وقودٌ في الخزّان إلى دَين في الدفتر.
|
||||
*
|
||||
* تُنادى من نظام المحطة لا من تطبيق السائق. السائق طرف مستفيد، وجعل
|
||||
* تأكيد الصرف بيده يعني أن ينفي ما استلمه.
|
||||
*
|
||||
* لا تخصم شيئاً من رصيد أحد — تقيّد استحقاقاً، ويحصّله محرك التسوية
|
||||
* بالسقف اليومي. نفس الفصل الذي يحكم التأمين.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
require_once __DIR__ . '/functions.php';
|
||||
|
||||
try {
|
||||
$con = Database::get('main');
|
||||
} catch (Exception $e) {
|
||||
error_log('[fuel/redeem] DB unavailable: ' . $e->getMessage());
|
||||
jsonError('Server error', 500);
|
||||
}
|
||||
|
||||
// مصادقة المحطة بمفتاحها الخاص لا بمفتاح النظام المشترك: المفاتيح
|
||||
// موزّعة على أطراف خارجية، وتسريب مفتاح محطة يجب أن يكشف تلك المحطة
|
||||
// وحدها ويُبطَل بصفّ واحد.
|
||||
$stationKey = $_SERVER['HTTP_X_STATION_KEY'] ?? '';
|
||||
if (empty($stationKey)) {
|
||||
jsonError('Unauthorized', 401);
|
||||
}
|
||||
|
||||
$st = $con->prepare("
|
||||
SELECT id, name_ar, discount_percent
|
||||
FROM fuel_stations
|
||||
WHERE api_key_hash = ? AND is_active = 1 LIMIT 1
|
||||
");
|
||||
$st->execute([hash('sha256', $stationKey)]);
|
||||
$station = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$station) {
|
||||
jsonError('Unauthorized', 401);
|
||||
}
|
||||
|
||||
$code = filterRequest('code');
|
||||
$amount = (float) (filterRequest('amount', 'numeric') ?: 0);
|
||||
|
||||
if (empty($code) || $amount <= 0) {
|
||||
jsonError('code و amount مطلوبان');
|
||||
}
|
||||
|
||||
try {
|
||||
$con->beginTransaction();
|
||||
|
||||
// القفل ضروري لا احتياطي: طلبان متزامنان بالرمز نفسه (ضغطة مكرّرة
|
||||
// على جهاز المحطة، أو محاولة صرف مزدوج) كانا سيمرّان كلاهما فيُقيَّد
|
||||
// الدَّين مرتين على وقود صُرف مرة.
|
||||
$st = $con->prepare("
|
||||
SELECT v.*, o.driver_id AS o_driver
|
||||
FROM fuel_vouchers v
|
||||
JOIN driver_obligations o ON o.id = v.obligation_id
|
||||
WHERE v.code = ? FOR UPDATE
|
||||
");
|
||||
$st->execute([$code]);
|
||||
$voucher = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$voucher) {
|
||||
$con->rollBack();
|
||||
jsonError('رمز غير معروف');
|
||||
}
|
||||
|
||||
if ($voucher['status'] !== 'issued') {
|
||||
$con->rollBack();
|
||||
// رسالة صريحة لا عامة: عامل المحطة يحتاج أن يعرف إن كانت صُرفت
|
||||
// سابقاً أم انتهت صلاحيتها، وإلا أعاد المحاولة أمام سائق ينتظر.
|
||||
jsonError('القسيمة ' . match ($voucher['status']) {
|
||||
'redeemed' => 'مصروفة سابقاً',
|
||||
'expired' => 'منتهية الصلاحية',
|
||||
'cancelled' => 'ملغاة',
|
||||
default => 'غير قابلة للصرف',
|
||||
});
|
||||
}
|
||||
|
||||
if (strtotime($voucher['expires_at']) < time()) {
|
||||
$con->prepare("UPDATE fuel_vouchers SET status = 'expired' WHERE id = ?")
|
||||
->execute([(int) $voucher['id']]);
|
||||
$con->commit();
|
||||
jsonError('القسيمة منتهية الصلاحية');
|
||||
}
|
||||
|
||||
// الصرف لا يتجاوز المأذون. تجاوزه يعني ديناً لم يوافق عليه السائق
|
||||
// ولم يُحجز له سقف — أي خرقاً للسقف من باب خلفي.
|
||||
if ($amount > (float) $voucher['amount_authorized']) {
|
||||
$con->rollBack();
|
||||
jsonError(
|
||||
'المبلغ يتجاوز المأذون به في القسيمة ('
|
||||
. (float) $voucher['amount_authorized'] . ')'
|
||||
);
|
||||
}
|
||||
|
||||
$con->prepare("
|
||||
UPDATE fuel_vouchers
|
||||
SET status = 'redeemed', station_id = ?, amount_redeemed = ?,
|
||||
discount_percent = ?, redeemed_at = NOW()
|
||||
WHERE id = ?
|
||||
")->execute([
|
||||
(int) $station['id'], $amount,
|
||||
(float) $station['discount_percent'], (int) $voucher['id'],
|
||||
]);
|
||||
|
||||
// قيد الاستحقاق. `source_ref` رقم القسيمة — وهو ما يسمح بأكثر من
|
||||
// سحبة في اليوم الواحد (المفتاح الفريد يشمله)، ويجعل إعادة النداء
|
||||
// بالرمز نفسه غير قادرة على مضاعفة الدَّين.
|
||||
$con->prepare("
|
||||
INSERT INTO obligation_ledger
|
||||
(obligation_id, driver_id, product_code, charge_date, source_ref,
|
||||
amount, amount_collected, amount_remaining, currency, status, note)
|
||||
VALUES (?, ?, ?, CURDATE(), ?, ?, 0, ?, ?, 'pending', ?)
|
||||
")->execute([
|
||||
(int) $voucher['obligation_id'], $voucher['driver_id'], FUEL_PRODUCT_CODE,
|
||||
'voucher_' . (int) $voucher['id'],
|
||||
$amount, $amount, $voucher['currency'],
|
||||
mb_substr('تزوّد من ' . $station['name_ar'], 0, 255),
|
||||
]);
|
||||
|
||||
$con->commit();
|
||||
} catch (PDOException $e) {
|
||||
if ($con->inTransaction()) $con->rollBack();
|
||||
|
||||
// 23000 على المفتاح الفريد يعني أن هذه القسيمة قُيّدت فعلاً — نداء
|
||||
// مكرّر لا خطأ. القسيمة مصروفة والدَّين مقيَّد مرة واحدة.
|
||||
if ($e->getCode() === '23000') {
|
||||
jsonSuccess(['code' => $code, 'duplicate' => true], 'القسيمة مصروفة سابقاً');
|
||||
}
|
||||
|
||||
error_log('[fuel/redeem] ' . $e->getMessage());
|
||||
jsonError('تعذّر إتمام الصرف', 500);
|
||||
} catch (Throwable $e) {
|
||||
if ($con->inTransaction()) $con->rollBack();
|
||||
error_log('[fuel/redeem] ' . $e->getMessage());
|
||||
jsonError('تعذّر إتمام الصرف', 500);
|
||||
}
|
||||
|
||||
error_log("[fuel] صُرفت القسيمة {$code} في محطة #{$station['id']} بقيمة $amount");
|
||||
|
||||
jsonSuccess([
|
||||
'code' => $code,
|
||||
'amount' => $amount,
|
||||
'currency' => $voucher['currency'],
|
||||
'station' => $station['name_ar'],
|
||||
'discount_percent' => (float) $station['discount_percent'],
|
||||
], 'تم الصرف');
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* fuel/request_voucher.php — إصدار قسيمة تزوّد
|
||||
*
|
||||
* السائق يطلب مبلغاً فيحصل على رمز يعطيه لعامل المحطة. لا دَين بعد —
|
||||
* القسيمة حجزٌ من السقف فقط، والدَّين يُقيَّد عند تأكيد المحطة الصرف.
|
||||
*
|
||||
* لماذا قسيمة مسبقة لا خصم عند المضخة؟ لأن عامل المحطة لا يملك وسيلة
|
||||
* للتحقق من هوية السائق ولا من سقفه. القسيمة تنقل القرار إلى تطبيق
|
||||
* السائق حيث الهوية مؤكَّدة والسقف معلوم، ولا يبقى على المحطة إلا
|
||||
* تأكيد رقم.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
require_once __DIR__ . '/functions.php';
|
||||
|
||||
$driverId = $user_id ?? '';
|
||||
if (empty($driverId) || ($role ?? '') !== 'driver') {
|
||||
jsonError('Unauthorized', 401);
|
||||
}
|
||||
|
||||
$amount = (float) (filterRequest('amount', 'numeric') ?: 0);
|
||||
if ($amount <= 0) {
|
||||
jsonError('حدّد مبلغ التزوّد');
|
||||
}
|
||||
|
||||
$obligation = fuelObligation($con, $driverId);
|
||||
if (!$obligation) {
|
||||
jsonError('فعّل محفظة الوقود أولاً');
|
||||
}
|
||||
|
||||
// تحرير المنتهي قبل الحساب: بدونه يبقى المبلغ محجوزاً بقسيمة ماتت،
|
||||
// فيُمنع السائق من التزوّد بسبب طلب نسيه قبل ساعة.
|
||||
fuelExpireStaleVouchers($con, $driverId);
|
||||
|
||||
// قسيمة مفتوحة واحدة في كل وقت. قسيمتان تعنيان رمزين في يد السائق،
|
||||
// وخطأ عامل المحطة في اختيار أحدهما يصرف المبلغ الخطأ — والأهم أن
|
||||
// السائق لا يحتاج أكثر من واحدة أصلاً.
|
||||
$st = $con->prepare("
|
||||
SELECT id, code, amount_authorized, expires_at
|
||||
FROM fuel_vouchers
|
||||
WHERE driver_id = ? AND status = 'issued' AND expires_at >= NOW()
|
||||
LIMIT 1
|
||||
");
|
||||
$st->execute([$driverId]);
|
||||
if ($open = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||
jsonError(
|
||||
'لديك قسيمة سارية بالرمز ' . $open['code'] . ' بقيمة '
|
||||
. (float) $open['amount_authorized'] . '. استخدمها أو ألغِها أولاً.'
|
||||
);
|
||||
}
|
||||
|
||||
$state = fuelCreditState($con, $obligation);
|
||||
if ($amount > $state['available']) {
|
||||
jsonError(
|
||||
'المتاح في محفظة وقودك ' . $state['available'] . ' ' . $obligation['currency']
|
||||
. '. سدّد المستحق عليك ليعود سقفك.'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$code = fuelGenerateCode($con);
|
||||
|
||||
$con->prepare("
|
||||
INSERT INTO fuel_vouchers
|
||||
(obligation_id, driver_id, code, amount_authorized, currency, status, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'issued', DATE_ADD(NOW(), INTERVAL ? MINUTE))
|
||||
")->execute([
|
||||
(int) $obligation['id'], $driverId, $code, $amount,
|
||||
$obligation['currency'], FUEL_VOUCHER_TTL_MIN,
|
||||
]);
|
||||
|
||||
$voucherId = (int) $con->lastInsertId();
|
||||
} catch (Throwable $e) {
|
||||
error_log('[fuel/request_voucher] ' . $e->getMessage());
|
||||
jsonError('تعذّر إصدار القسيمة حالياً');
|
||||
}
|
||||
|
||||
error_log("[fuel] قسيمة #$voucherId للسائق $driverId بقيمة $amount");
|
||||
|
||||
jsonSuccess([
|
||||
'voucher_id' => $voucherId,
|
||||
'code' => $code,
|
||||
'amount' => $amount,
|
||||
'currency' => $obligation['currency'],
|
||||
'expires_in_min' => FUEL_VOUCHER_TTL_MIN,
|
||||
'available_after' => round($state['available'] - $amount, 3),
|
||||
], 'أعطِ هذا الرمز لعامل المحطة');
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* fuel/stations.php — المحطات الشريكة
|
||||
*
|
||||
* قائمة للسائق، مرتّبة بالقرب إن أرسل موقعه. لا يُرجع مفاتيح المحطات
|
||||
* ولا أي حقل تشغيلي — القائمة تُعرض في تطبيق السائق، وكل ما يخرج إليه
|
||||
* يجب افتراض أنه علنيّ.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../connect.php';
|
||||
require_once __DIR__ . '/functions.php';
|
||||
|
||||
$driverId = $user_id ?? '';
|
||||
if (empty($driverId) || ($role ?? '') !== 'driver') {
|
||||
jsonError('Unauthorized', 401);
|
||||
}
|
||||
|
||||
$lat = filterRequest('lat', 'numeric');
|
||||
$lng = filterRequest('lng', 'numeric');
|
||||
|
||||
try {
|
||||
if ($lat !== null && $lng !== null) {
|
||||
// مسافة تقريبية بفرق الإحداثيات لا Haversine: الغرض ترتيب قائمة
|
||||
// محطات في مدينة واحدة، والفارق بين الطريقتين على هذا النطاق
|
||||
// لا يغيّر ترتيباً — بينما الحساب المثلثي على كل صف يكلّف بلا مقابل.
|
||||
$st = $con->prepare("
|
||||
SELECT id, name_ar, chain_name, city, address, lat, lng, phone,
|
||||
discount_percent,
|
||||
(POW(lat - ?, 2) + POW(lng - ?, 2)) AS dist
|
||||
FROM fuel_stations
|
||||
WHERE is_active = 1
|
||||
ORDER BY (lat IS NULL), dist ASC
|
||||
LIMIT 50
|
||||
");
|
||||
$st->execute([(float) $lat, (float) $lng]);
|
||||
} else {
|
||||
$st = $con->query("
|
||||
SELECT id, name_ar, chain_name, city, address, lat, lng, phone,
|
||||
discount_percent
|
||||
FROM fuel_stations
|
||||
WHERE is_active = 1
|
||||
ORDER BY city ASC, name_ar ASC
|
||||
LIMIT 50
|
||||
");
|
||||
}
|
||||
|
||||
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
error_log('[fuel/stations] ' . $e->getMessage());
|
||||
jsonError('Server error');
|
||||
}
|
||||
|
||||
$out = array_map(static fn(array $r): array => [
|
||||
'id' => (int) $r['id'],
|
||||
'name' => $r['name_ar'],
|
||||
'chain' => $r['chain_name'],
|
||||
'city' => $r['city'],
|
||||
'address' => $r['address'],
|
||||
'lat' => $r['lat'] !== null ? (float) $r['lat'] : null,
|
||||
'lng' => $r['lng'] !== null ? (float) $r['lng'] : null,
|
||||
'phone' => $r['phone'],
|
||||
'discount_percent' => (float) $r['discount_percent'],
|
||||
], $rows);
|
||||
|
||||
jsonSuccess(['stations' => $out], 'success');
|
||||
Reference in New Issue
Block a user