Update: 2026-08-03 11:32:12
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// ride/driverPayment/withdrawal_requests.php
|
||||
// قائمة طلبات سحب أرصدة السائقين للوحة التحكم
|
||||
// ============================================================
|
||||
//
|
||||
// لماذا هذا الملف:
|
||||
// جدول `driver_withdrawal_requests` يُكتب فيه من
|
||||
// ride/mtn/driver_payout_syria.php عند طلب السائق السحب، لكن **لا شيء في
|
||||
// المنصّة كلها يقرأه** — لا نقطة في الباك إند ولا شاشة في لوحة التحكم
|
||||
// (تحقّقت بمسح كامل على backend/ و payment_server/ و siro_admin/).
|
||||
// أي أن طلبات السحب كانت تتراكم دون أن يراها أحد، والإشعار الوحيد رسالة
|
||||
// واتساب لحظية عند الإرسال — تضيع إن فات وقتها.
|
||||
//
|
||||
// GET : قائمة الطلبات مع تصفية بالحالة + مجاميع لكل حالة
|
||||
// POST : تحديث حالة طلب (approved / rejected / paid)
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
$role = $decodedToken->role ?? null;
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => 'Forbidden. Admin access required.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// الحالات المسموح بها — قائمة مغلقة حتى لا تُكتب حالة عشوائية في الجدول
|
||||
const WITHDRAWAL_STATUSES = ['pending', 'approved', 'rejected', 'paid'];
|
||||
|
||||
// التوجيه بحقل action لا بطريقة HTTP: عميل اللوحة (CRUD.getWallet في
|
||||
// siro_admin) يرسل كل الطلبات بـ POST form-encoded، فالتفريق بالطريقة كان
|
||||
// سيوجّه طلبات العرض إلى فرع التحديث.
|
||||
$body = $_POST;
|
||||
if (empty($body)) {
|
||||
$raw = json_decode(file_get_contents('php://input'), true);
|
||||
if (is_array($raw)) $body = $raw;
|
||||
}
|
||||
$action = $body['action'] ?? $_GET['action'] ?? 'list';
|
||||
|
||||
try {
|
||||
// ── تحديث حالة طلب ───────────────────────────────────────
|
||||
if ($action === 'update_status') {
|
||||
// صرف المال فعلياً قرار لا رجعة فيه — نقصره على super_admin
|
||||
if ($role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => 'Forbidden. Super Admin required to change withdrawal status.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = isset($body['id']) ? (int)$body['id'] : 0;
|
||||
$status = trim((string)($body['status'] ?? ''));
|
||||
|
||||
if ($id <= 0 || !in_array($status, WITHDRAWAL_STATUSES, true)) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => 'Invalid id or status. Allowed: ' . implode(', ', WITHDRAWAL_STATUSES),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// لا نسمح بتعديل طلب خرج من pending إلا إلى paid — يمنع عكس رفض
|
||||
// أو إعادة اعتماد طلب مدفوع بالخطأ.
|
||||
$cur = $con->prepare("SELECT status FROM driver_withdrawal_requests WHERE id = ?");
|
||||
$cur->execute([$id]);
|
||||
$current = $cur->fetchColumn();
|
||||
|
||||
if ($current === false) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Request not found.'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
if ($current !== 'pending' && !($current === 'approved' && $status === 'paid')) {
|
||||
http_response_code(409);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => "Cannot change status from '$current' to '$status'.",
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$upd = $con->prepare("UPDATE driver_withdrawal_requests SET status = ? WHERE id = ?");
|
||||
$upd->execute([$status, $id]);
|
||||
|
||||
error_log("[withdrawal_requests] id=$id '$current' -> '$status' by " . ($decodedToken->user_id ?? 'unknown'));
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => ['id' => $id, 'from' => $current, 'to' => $status],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── قائمة الطلبات ────────────────────────────────────────
|
||||
$filter = $body['status'] ?? $_GET['status'] ?? 'pending';
|
||||
$limit = (int)($body['limit'] ?? $_GET['limit'] ?? 100);
|
||||
$limit = max(1, min(200, $limit));
|
||||
|
||||
$where = '';
|
||||
$params = [];
|
||||
if ($filter !== 'all') {
|
||||
if (!in_array($filter, WITHDRAWAL_STATUSES, true)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['status' => 'failure', 'message' => 'Invalid status filter.'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
$where = 'WHERE status = :status';
|
||||
$params = [':status' => $filter];
|
||||
}
|
||||
|
||||
$stmt = $con->prepare("
|
||||
SELECT id, driver_id, driver_name, amount, wallet_type, wallet_number,
|
||||
status, created_at, updated_at
|
||||
FROM driver_withdrawal_requests
|
||||
$where
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $limit
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$r['amount'] = (float)$r['amount'];
|
||||
$r['id'] = (int)$r['id'];
|
||||
}
|
||||
unset($r);
|
||||
|
||||
// مجاميع لكل حالة — تغذّي شارات العدّ في اللوحة دون طلب إضافي
|
||||
$totals = [];
|
||||
$agg = $con->query("
|
||||
SELECT status, COUNT(*) AS cnt, COALESCE(SUM(amount), 0) AS total
|
||||
FROM driver_withdrawal_requests
|
||||
GROUP BY status
|
||||
");
|
||||
foreach ($agg->fetchAll(PDO::FETCH_ASSOC) as $a) {
|
||||
$totals[$a['status']] = [
|
||||
'count' => (int)$a['cnt'],
|
||||
'total' => (float)$a['total'],
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'filter' => $filter,
|
||||
'requests' => $rows,
|
||||
'totals' => $totals,
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('[withdrawal_requests] ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => 'An internal error occurred.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// ride/siroWallet/summary.php
|
||||
// ملخّص محفظة سيرو — شهري وإجمالي مع تفصيل الإيداع والصرف
|
||||
// ============================================================
|
||||
//
|
||||
// لماذا ملف جديد بدل تعديل get.php:
|
||||
// • get.php يُرجع SUM(amount) واحداً للشهر الجاري فقط، بعمود بلا اسم
|
||||
// (المفتاح يصل للعميل كـ "SUM(amount)")، ولا يفرّق بين الإيداع والصرف.
|
||||
// • get.php يستدعي printSuccess()/printFailure() وهما غير معرّفتين في
|
||||
// payment_server/v2 إطلاقاً (لا في functions.php ولا في connect.php) —
|
||||
// معرّفتان محلياً داخل ملفَّي syriatel فقط. أي أن get.php يسقط بخطأ
|
||||
// "undefined function" فور اجتياز المصادقة.
|
||||
// • فحص حيّ في 2026-08-03: get.php يُرجع 404 على v1 و v2 معاً — أي أنه
|
||||
// غير منشور أصلاً، بينما driverWallet/get.php يُرجع 401 (موجود).
|
||||
//
|
||||
// لذلك هذا الملف مكتفٍ بذاته: لا يعتمد إلا على connect.php و json_encode.
|
||||
//
|
||||
// الرد:
|
||||
// month_to_date / previous_month / all_time، ولكلٍّ:
|
||||
// credits → مجموع الحركات الموجبة (إيداع)
|
||||
// debits → مجموع الحركات السالبة (صرف/سحب) كقيمة موجبة
|
||||
// net → الصافي
|
||||
// tx_count → عدد الحركات
|
||||
// by_payment_method → تفصيل الشهر الجاري حسب وسيلة الدفع
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
|
||||
// connect.php ينفّذ authenticateJWT() ويُسقط الطلب عند الفشل، فالوصول إلى
|
||||
// هنا يعني توكناً صالحاً. نقصر الملخّص المالي على الأدوار الإدارية.
|
||||
$role = $decodedToken->role ?? null;
|
||||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => 'Forbidden. Admin access required.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* مجاميع نافذة زمنية واحدة.
|
||||
* الحركات السالبة تُعاد كقيمة موجبة في debits ليقرأها العميل مباشرة.
|
||||
*/
|
||||
function siroWalletWindow(PDO $con, ?string $from, ?string $to): array
|
||||
{
|
||||
$where = '';
|
||||
$params = [];
|
||||
if ($from !== null && $to !== null) {
|
||||
$where = 'WHERE createdAt >= :from AND createdAt < :to';
|
||||
$params = [':from' => $from, ':to' => $to];
|
||||
}
|
||||
|
||||
$stmt = $con->prepare("
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END), 0) AS credits,
|
||||
COALESCE(SUM(CASE WHEN amount < 0 THEN -amount ELSE 0 END), 0) AS debits,
|
||||
COALESCE(SUM(amount), 0) AS net,
|
||||
COUNT(*) AS tx_count
|
||||
FROM `siroWallet`
|
||||
$where
|
||||
");
|
||||
$stmt->execute($params);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
return [
|
||||
'credits' => (float)($row['credits'] ?? 0),
|
||||
'debits' => (float)($row['debits'] ?? 0),
|
||||
'net' => (float)($row['net'] ?? 0),
|
||||
'tx_count' => (int) ($row['tx_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
// حدود الشهر تُحسب في PHP لا في SQL، حتى تتبع المنطقة الزمنية للتطبيق
|
||||
// ويسهل اختبارها، ولتفادي BETWEEN على TIMESTAMP الذي يُسقط آخر يوم.
|
||||
$startThisMonth = date('Y-m-01 00:00:00');
|
||||
$startNextMonth = date('Y-m-01 00:00:00', strtotime('first day of next month'));
|
||||
$startPrevMonth = date('Y-m-01 00:00:00', strtotime('first day of last month'));
|
||||
|
||||
$monthToDate = siroWalletWindow($con, $startThisMonth, $startNextMonth);
|
||||
$previousMonth = siroWalletWindow($con, $startPrevMonth, $startThisMonth);
|
||||
$allTime = siroWalletWindow($con, null, null);
|
||||
|
||||
// تفصيل الشهر الجاري حسب وسيلة الدفع
|
||||
$stmt = $con->prepare("
|
||||
SELECT
|
||||
paymentMethod,
|
||||
COALESCE(SUM(amount), 0) AS net,
|
||||
COUNT(*) AS tx_count
|
||||
FROM `siroWallet`
|
||||
WHERE createdAt >= :from AND createdAt < :to
|
||||
GROUP BY paymentMethod
|
||||
ORDER BY net DESC
|
||||
");
|
||||
$stmt->execute([':from' => $startThisMonth, ':to' => $startNextMonth]);
|
||||
|
||||
$byMethod = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$byMethod[] = [
|
||||
'payment_method' => $r['paymentMethod'] !== '' ? $r['paymentMethod'] : 'unknown',
|
||||
'net' => (float)$r['net'],
|
||||
'tx_count' => (int)$r['tx_count'],
|
||||
];
|
||||
}
|
||||
|
||||
// نسبة التغيّر عن الشهر السابق — الواجهة تعرضها كمؤشّر اتجاه
|
||||
$prevNet = $previousMonth['net'];
|
||||
$growth = ($prevNet != 0.0)
|
||||
? round((($monthToDate['net'] - $prevNet) / abs($prevNet)) * 100, 2)
|
||||
: null;
|
||||
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'month_to_date' => $monthToDate,
|
||||
'previous_month' => $previousMonth,
|
||||
'all_time' => $allTime,
|
||||
'growth_percent' => $growth, // null عندما لا يوجد أساس للمقارنة
|
||||
'by_payment_method' => $byMethod,
|
||||
'period' => [
|
||||
'month_start' => $startThisMonth,
|
||||
'generated_at' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
error_log('[siroWallet/summary] ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'failure',
|
||||
'message' => 'An internal error occurred.',
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
Reference in New Issue
Block a user