Update: 2026-08-08 12:58:16
This commit is contained in:
@@ -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'],
|
||||
], 'تم الصرف');
|
||||
Reference in New Issue
Block a user