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