Files
Siro/backend/fuel/redeem.php
T

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'],
], 'تم الصرف');