prepare(" SELECT p.id, p.type, p.amount FROM food_order_payments p JOIN food_orders o ON o.id = p.order_id WHERE o.courier_id = ? AND o.delivered_at BETWEEN ? AND ? AND p.settlement_id IS NULL AND p.type IN ('courier_payout', 'cash_settlement') "); $st->execute([$courierId, $periodStart, $periodEnd]); $rows = $st->fetchAll(PDO::FETCH_ASSOC); $payoutTotal = 0; $cashDebt = 0; $payoutIds = []; foreach ($rows as $r) { $payoutIds[] = (int) $r['id']; if ($r['type'] === 'courier_payout') { $payoutTotal += (int) $r['amount']; } else { $cashDebt += (int) $r['amount']; } } // ══════════════════════════════════════════════════════════ // ٢) المُرحَّل من دورة سابقة سالبة // ══════════════════════════════════════════════════════════ $stCarry = $food_con->prepare(" SELECT COALESCE(SUM(-net_amount), 0) FROM food_courier_settlements WHERE courier_id = ? AND status = 'carried' "); $stCarry->execute([$courierId]); $carriedOver = (int) $stCarry->fetchColumn(); $net = $payoutTotal - $cashDebt - $carriedOver; $stCount = $food_con->prepare(" SELECT COUNT(*) FROM food_orders WHERE courier_id = ? AND status = 'delivered' AND delivered_at BETWEEN ? AND ? "); $stCount->execute([$courierId, $periodStart, $periodEnd]); $ordersCount = (int) $stCount->fetchColumn(); $summary = [ 'courier_id' => $courierId, 'period_start' => $periodStart, 'period_end' => $periodEnd, 'orders_count' => $ordersCount, 'payout_total' => $payoutTotal, 'cash_debt_total' => $cashDebt, 'carried_over' => $carriedOver, 'net_amount' => $net, 'net_decimal' => foodSmallestUnitToDecimal(abs($net)) * ($net < 0 ? -1 : 1), 'currency' => $currency, 'entries_count' => count($payoutIds), ]; // ── المعاينة تتوقف هنا ────────────────────────────────── if ($action === 'preview') { $summary['action'] = 'preview'; $summary['would_pay'] = $net > 0; jsonSuccess($summary, 'Settlement preview'); } // ══════════════════════════════════════════════════════════ // ٣) التنفيذ // ══════════════════════════════════════════════════════════ if (empty($payoutIds) && $carriedOver === 0) { jsonError('No unsettled entries for this courier and period'); } $status = $net > 0 ? 'draft' : 'carried'; $food_con->beginTransaction(); $ins = $food_con->prepare(" INSERT INTO food_courier_settlements (courier_id, period_start, period_end, orders_count, payout_total, cash_debt_total, carried_over, net_amount, currency, status, executed_by, executed_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,NOW()) "); $ins->execute([ $courierId, $periodStart, $periodEnd, $ordersCount, $payoutTotal, $cashDebt, $carriedOver, $net, $currency, $status, $food_admin_id ?: 'admin', ]); $settlementId = (int) $food_con->lastInsertId(); // ‏وسم القيود قبل الصرف: لو انقطع التنفيذ بعد التحويل وقبل الوسم، // ‏لأُعيد صرفها في الدورة التالية. if (!empty($payoutIds)) { $ph = implode(',', array_fill(0, count($payoutIds), '?')); $food_con->prepare(" UPDATE food_order_payments SET settlement_id = ?, status = 'success' WHERE id IN ($ph) ")->execute(array_merge([$settlementId], $payoutIds)); } // ‏الدورات السالبة المُرحَّلة استُهلكت في هذا الحساب — نغلقها حتى لا // ‏تُطرح مرة أخرى في الدورة القادمة. if ($carriedOver > 0) { $food_con->prepare(" UPDATE food_courier_settlements SET status = 'settled_forward', note = CONCAT(COALESCE(note,''), ' → #', ?) WHERE courier_id = ? AND status = 'carried' AND id <> ? ")->execute([$settlementId, $courierId, $settlementId]); } $food_con->commit(); // ══════════════════════════════════════════════════════════ // ٤) التحويل — بعد الـcommit لا داخله // ══════════════════════════════════════════════════════════ $transferCode = null; if ($net > 0) { $transferCode = foodPayCourier( $courierId, foodSmallestUnitToDecimal($net), $settlementId ); $food_con->prepare(" UPDATE food_courier_settlements SET status = ?, transfer_code = ? WHERE id = ? ")->execute([$transferCode === 200 ? 'paid' : 'failed', $transferCode, $settlementId]); if ($transferCode !== 200) { error_log("[FOOD][SETTLEMENT] MONEY: تسوية #$settlementId بقيمة $net" . " للسائق $courierId لم تصل (رمز=$transferCode)"); } } $summary['settlement_id'] = $settlementId; $summary['status'] = $net > 0 ? ($transferCode === 200 ? 'paid' : 'failed') : 'carried'; $summary['transfer_code'] = $transferCode; jsonSuccess($summary, 'Settlement executed'); } catch (PDOException $e) { if (isset($food_con) && $food_con->inTransaction()) $food_con->rollBack(); error_log("[FOOD][SETTLEMENT] " . $e->getMessage()); jsonError('DB Error', 500); } /** * ‏يودع صافي التسوية في محفظة السائق. * * ‏يستخدم driverWallet/add_s2s_reward.php — وهو العقد الذي أكّدناه عملياً * ‏في تعويض عدم الحضور ورسوم الإلغاء وتعويضات الشكاوى. تعليق delivered.php * ‏يقول إن العقد المؤكد لتحويلات سائق↔سائق فقط، وهذا لم يعد صحيحاً. * * ‏paymentID مشتق من رقم التسوية فيمنع ازدواج الصرف عند إعادة المحاولة. */ function foodPayCourier(string $courierId, float $amount, int $settlementId): int { $walletServer = foodWalletServerUrl(); $url = "$walletServer/v2/main/ride/driverWallet/add_s2s_reward.php"; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query([ 'driverID' => $courierId, 'paymentID' => "food_settlement_$settlementId", 'amount' => $amount, 'paymentMethod' => 'food_courier_settlement', ]), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'Content-Type: application/x-www-form-urlencoded', 'X-S2S-Api-Key: ' . getenv('S2S_SHARED_KEY'), ], ]); curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return $code; }