153 lines
6.4 KiB
PHP
153 lines
6.4 KiB
PHP
<?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'],
|
|
], 'تم الصرف');
|