100) { printFailure('cap_percent must be between 0 and 100'); exit; } /** * ‏يُرجع محاولة سابقة بنفس المرجع إن وُجدت. * * ‏هذه الدالة هي كل ضمان عدم التكرار: تُستدعى قبل أي عمل، وتُستدعى * ‏ثانيةً عند خرق المفتاح الفريد — لأن بين الفحص والكتابة نافذة، ونسختان * ‏من الكرون قد تعملان معاً. */ function obligationPriorAttempt(PDO $con, string $ref): ?array { $st = $con->prepare(" SELECT deducted_amount, requested_amount, day_earnings, cap_percent FROM obligation_settlements WHERE settlement_ref = ? LIMIT 1 "); $st->execute([$ref]); $row = $st->fetch(PDO::FETCH_ASSOC); return $row ?: null; } function obligationReplay(array $prior, string $ref): void { printSuccess([ 'settlement_ref' => $ref, 'deducted' => (float) $prior['deducted_amount'], 'requested' => (float) $prior['requested_amount'], 'day_earnings' => (float) $prior['day_earnings'], 'cap_percent' => (float) $prior['cap_percent'], 'replayed' => true, ]); } try { if ($prior = obligationPriorAttempt($con, $settlementRef)) { obligationReplay($prior, $settlementRef); exit; } // ‏الرصيد الحالي: driverWallet دفتر حركات لا رصيداً مخزّناً — الرصيد // ‏هو مجموع صفوفه، والخصم صفّ بمبلغ سالب. $st = $con->prepare("SELECT COALESCE(SUM(amount), 0) FROM driverWallet WHERE driverID = ?"); $st->execute([$driverID]); $balance = (float) $st->fetchColumn(); // ‏أرباح اليوم = الصفوف الموجبة فقط. الأساس هو ما دخل اليوم لا الرصيد // ‏المتراكم: الخصم من رصيد قديم يفاجئ سائقاً لم يعمل اليوم، والخصم من // ‏دخل اليوم يبقى محسوساً كاقتطاع من كسبٍ حاضر. $st = $con->prepare(" SELECT COALESCE(SUM(amount), 0) FROM driverWallet WHERE driverID = ? AND amount > 0 AND DATE(dateCreated) = CURDATE() "); $st->execute([$driverID]); $dayEarnings = (float) $st->fetchColumn(); // ‏ما اقتطعه المحرك اليوم من كل المنتجات. بدون هذا الطرح يصير السقف // ‏لكل منتج على حدة: ثلاثة منتجات بسقف ٢٥٪ تأخذ ٧٥٪ من يوم السائق. $st = $con->prepare(" SELECT COALESCE(SUM(deducted_amount), 0) FROM obligation_settlements WHERE driver_id = ? AND DATE(created_at) = CURDATE() "); $st->execute([$driverID]); $takenToday = (float) $st->fetchColumn(); $allowance = round(($dayEarnings * $capPercent / 100) - $takenToday, 3); // ‏أرضية الرصيد: لا يدفع المحرك سائقاً إلى السالب مهما استحقّ عليه. // ‏الدَّين المرحَّل خيار السائق يواصل به العمل؛ الرصيد السالب المفاجئ // ‏يوقفه عن العمل، فيتعذّر السداد أصلاً. $minBalance = (float) (getenv('OBLIGATION_MIN_BALANCE') ?: 0); $headroom = round($balance - $minBalance, 3); $deducted = min($amount, $allowance, $headroom); if ($deducted < 0) { $deducted = 0.0; } $deducted = round($deducted, 3); $con->beginTransaction(); // ‏السجل يُكتب دائماً — حتى حين لا يُخصم شيء. صفر مسجَّل يعني «حاولنا // ‏اليوم ولم يكن هناك ما يُخصم منه»، وهو ما يمنع ملاحقة السائق مراراً // ‏في اليوم نفسه ويشرح لاحقاً لماذا تأخّر التحصيل. $con->prepare(" INSERT INTO obligation_settlements (settlement_ref, driver_id, product_code, requested_amount, deducted_amount, day_earnings, cap_percent) VALUES (?, ?, ?, ?, ?, ?, ?) ")->execute([ $settlementRef, $driverID, $productCode, $amount, $deducted, $dayEarnings, $capPercent, ]); if ($deducted > 0) { // ‏paymentMethod عمود varchar(20) — لا يتّسع لاسم المنتج. التفصيل // ‏يعيش في paymentID (وهو settlement_ref) وفي جدول التسويات. $con->prepare(" INSERT INTO driverWallet (driverID, paymentID, amount, paymentMethod) VALUES (?, ?, ?, 'obligation') ")->execute([$driverID, $settlementRef, -$deducted]); } $con->commit(); printSuccess([ 'settlement_ref' => $settlementRef, 'deducted' => $deducted, 'requested' => $amount, 'day_earnings' => $dayEarnings, 'cap_percent' => $capPercent, 'balance_before' => $balance, 'replayed' => false, ]); } catch (PDOException $e) { if ($con->inTransaction()) { $con->rollBack(); } // ‏23000 = خرق المفتاح الفريد على settlement_ref: نسخة أخرى سبقتنا // ‏بالمرجع نفسه. ليست حالة خطأ — هي بالضبط ما صُمّم المفتاح لمنعه. if ($e->getCode() === '23000' && ($prior = obligationPriorAttempt($con, $settlementRef))) { obligationReplay($prior, $settlementRef); exit; } error_log('[obligation/deduct] ' . $e->getMessage()); http_response_code(500); printFailure('Settlement failed'); } catch (Exception $e) { if ($con->inTransaction()) { $con->rollBack(); } error_log('[obligation/deduct] ' . $e->getMessage()); http_response_code(500); printFailure('Settlement failed'); }