Update codebase
This commit is contained in:
@@ -12,7 +12,7 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $con->prepare("SELECT status FROM click_invoices WHERE invoice_number = :invoice_number LIMIT 1");
|
||||
$stmt = $con->prepare("SELECT status FROM cliq_invoices WHERE invoice_number = :invoice_number LIMIT 1");
|
||||
$stmt->execute([':invoice_number' => $invoiceNumber]);
|
||||
$invoice = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ try {
|
||||
|
||||
// 1. البحث عن الفاتورة وتحديث حالتها
|
||||
$stmt = $con->prepare(
|
||||
"UPDATE `click_invoices`
|
||||
"UPDATE `cliq_invoices`
|
||||
SET `status` = 'completed', `click_transaction_id` = :transaction_id
|
||||
WHERE `invoice_number` = :invoice_number AND `status` = 'pending'"
|
||||
);
|
||||
@@ -52,7 +52,7 @@ try {
|
||||
$invoiceId = $con->lastInsertId(); // ملاحظة: هذا قد لا يعمل دائماً مع UPDATE، الأفضل جلب الـ ID
|
||||
|
||||
// جلب ID الفاتورة بعد التأكد من وجودها
|
||||
$idStmt = $con->prepare("SELECT id FROM `click_invoices` WHERE `invoice_number` = :invoice_number");
|
||||
$idStmt = $con->prepare("SELECT id FROM `cliq_invoices` WHERE `invoice_number` = :invoice_number");
|
||||
$idStmt->execute([':invoice_number' => $invoiceNumber]);
|
||||
$invoiceRecord = $idStmt->fetch();
|
||||
$invoiceId = $invoiceRecord['id'];
|
||||
|
||||
@@ -7,11 +7,20 @@ try {
|
||||
$userId = filterRequest("user_id");
|
||||
$userType = filterRequest("user_type");
|
||||
$amount = filterRequest("amount");
|
||||
$cliqPhone = filterRequest("cliq_phone");
|
||||
$cliqPhone = filterRequest("cliq_phone") ?: filterRequest("click_phone");
|
||||
$phone = filterRequest("phone");
|
||||
|
||||
if (empty($userId) || empty($userType) || !is_numeric($amount) || $amount <= 0 || empty($cliqPhone)) {
|
||||
echo json_encode(["status" => "failure", "message" => "Invalid input provided."]);
|
||||
echo json_encode([
|
||||
"status" => "failure",
|
||||
"message" => "Invalid input provided.",
|
||||
"debug" => [
|
||||
"userId" => $userId,
|
||||
"userType" => $userType,
|
||||
"amount" => $amount,
|
||||
"cliqPhone" => $cliqPhone
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -37,14 +46,12 @@ try {
|
||||
$upd = $con->prepare("
|
||||
UPDATE cliq_invoices
|
||||
SET amount = :amount,
|
||||
phone = :phone,
|
||||
cliq_phone = :cliq_phone,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
");
|
||||
$upd->execute([
|
||||
':amount' => $amount,
|
||||
':phone' => $phone ?: null,
|
||||
':cliq_phone' => $cliqPhone,
|
||||
':id' => $existing['id'],
|
||||
]);
|
||||
@@ -62,15 +69,14 @@ try {
|
||||
|
||||
$ins = $con->prepare("
|
||||
INSERT INTO cliq_invoices
|
||||
(invoice_number, user_id, user_type, phone, amount, cliq_phone, status, created_at, updated_at)
|
||||
(invoice_number, user_id, user_type, amount, cliq_phone, status, created_at, updated_at)
|
||||
VALUES
|
||||
(:invoice_number, :user_id, :user_type, :phone, :amount, :cliq_phone, 'pending', NOW(), NOW())
|
||||
(:invoice_number, :user_id, :user_type, :amount, :cliq_phone, 'pending', NOW(), NOW())
|
||||
");
|
||||
$ins->execute([
|
||||
':invoice_number' => $invoiceNumber,
|
||||
':user_id' => $userId,
|
||||
':user_type' => $userType,
|
||||
':phone' => $phone ?: null,
|
||||
':amount' => $amount,
|
||||
':cliq_phone' => $cliqPhone
|
||||
]);
|
||||
|
||||
@@ -32,7 +32,7 @@ try {
|
||||
// البحث عن فاتورة معلقة لهذا الرقم
|
||||
$stmt = $con->prepare(
|
||||
"SELECT invoice_number, amount, user_id, user_type
|
||||
FROM `click_invoices`
|
||||
FROM `cliq_invoices`
|
||||
WHERE `click_phone` = :click_phone AND `status` = 'pending'
|
||||
ORDER BY `created_at` DESC LIMIT 1"
|
||||
);
|
||||
|
||||
@@ -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,198 @@
|
||||
<?php
|
||||
/**
|
||||
* deduct_s2s_obligation.php — خصم التزام من محفظة السائق
|
||||
* ─────────────────────────────────────────────────────────────
|
||||
* نقطة الخصم الوحيدة لمحرك الالتزامات (تأمين، وقود، صيانة، تمويل).
|
||||
*
|
||||
* لماذا لا نستخدم add_s2s_reward.php بمبلغ سالب؟
|
||||
*
|
||||
* ١. لا يضمن عدم التكرار. لا مفتاح فريد على paymentID ولا فحص إلا
|
||||
* لتحديات daily_/weekly_. إيداع مكرّر يُسترد، أما خصم مكرّر من
|
||||
* رصيد سائق فهو عطل لا يُصلحه اعتذار.
|
||||
*
|
||||
* ٢. السقف اليومي يجب أن يُحسب حيث تعيش البيانات. أرباح اليوم في
|
||||
* هذه القاعدة لا في القاعدة الرئيسية، وحسابه هناك كان سيعني
|
||||
* نقل الرصيد عبر الشبكة ثم اتخاذ قرار مالي على لقطة قديمة.
|
||||
*
|
||||
* العقد مع المحرك:
|
||||
* المدخل : settlement_ref, driverID, product_code, amount, cap_percent
|
||||
* المخرج : deducted (ما خُصم فعلاً — قد يكون أقل من المطلوب أو صفراً)
|
||||
*
|
||||
* الخصم الجزئي ليس فشلاً بل السلوك الطبيعي: السقف يحمي السائق، والمتبقي
|
||||
* يُرحَّل ليوم أفضل. المحرك هو من يقرّر ماذا يفعل بالمتبقي.
|
||||
*
|
||||
* إعادة الطلب بنفس settlement_ref آمنة دائماً: لا تخصم شيئاً وتُرجع
|
||||
* نتيجة المحاولة الأولى حرفياً. المحرك يبني المرجع على عدّاد محاولات
|
||||
* القيد لا على التاريخ، فنداء ضاع ردّه يُعاد بالمرجع نفسه مهما تأخّر.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../jwtconnect.php';
|
||||
|
||||
// jwtconnect يقبل ستة مسارات مصادقة. الخصم من مال السائق يقبل واحداً:
|
||||
// نداء خادم-لخادم. لا JWT سائق — لا معنى لأن يطلب سائق خصم نفسه.
|
||||
$providedKey = $_SERVER['HTTP_X_S2S_API_KEY'] ?? '';
|
||||
$expectedKey = getenv('S2S_SHARED_KEY') ?: '';
|
||||
if (empty($expectedKey) || empty($providedKey) || !hash_equals($expectedKey, $providedKey)) {
|
||||
http_response_code(401);
|
||||
printFailure('Unauthorized: Invalid or missing X-S2S-Api-Key.');
|
||||
exit;
|
||||
}
|
||||
|
||||
$settlementRef = filterRequest('settlement_ref');
|
||||
$driverID = filterRequest('driverID');
|
||||
$productCode = filterRequest('product_code');
|
||||
$amount = filterRequest('amount', 'numeric');
|
||||
$capPercent = filterRequest('cap_percent', 'numeric');
|
||||
|
||||
if (empty($settlementRef) || empty($driverID) || empty($productCode) || $amount === null) {
|
||||
printFailure('Missing required parameters: settlement_ref, driverID, product_code, amount');
|
||||
exit;
|
||||
}
|
||||
|
||||
$amount = round((float) $amount, 3);
|
||||
$capPercent = $capPercent === null ? 25.0 : (float) $capPercent;
|
||||
|
||||
if ($amount <= 0) {
|
||||
printFailure('amount must be greater than zero');
|
||||
exit;
|
||||
}
|
||||
// سقف بلا حدّ أعلى يعني تفريغ محفظة السائق في يوم واحد بضغطة إعداد
|
||||
// خاطئة. مئة بالمئة مسموحة كحدّ أقصى صريح لا كنتيجة خطأ مطبعي.
|
||||
if ($capPercent < 0 || $capPercent > 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');
|
||||
}
|
||||
@@ -1,7 +1,23 @@
|
||||
<?php
|
||||
include "../../connect.php";
|
||||
include_once "../../db_primary.php";
|
||||
|
||||
$driverID = filterRequest("driverID");
|
||||
|
||||
/**
|
||||
* `driver_orders` و`ride` كلاهما في القاعدة الأساسية لا في قاعدة الدفع —
|
||||
* نفس علّة getAllPayment.php التي كانت تُسقط الرصيد بـ
|
||||
* `Table 'payment.ride' doesn't exist`.
|
||||
*
|
||||
* كل جداول هذا الاستعلام في القاعدة الأساسية، فيُنفَّذ عليها كاملاً عبر
|
||||
* اتصال مستقل — لا إشارة مؤهَّلة، لأن القاعدتين قد تكونان على خادمين.
|
||||
*/
|
||||
$primary = primaryDbConnection();
|
||||
if ($primary === null) {
|
||||
printFailure("Statistics unavailable");
|
||||
exit;
|
||||
}
|
||||
|
||||
$sql = "SELECT
|
||||
YEAR(`driver_orders`.`created_at`) AS `year`,
|
||||
MONTH(`driver_orders`.`created_at`) AS `month`,
|
||||
@@ -30,19 +46,25 @@ ORDER BY
|
||||
|
||||
";
|
||||
// كان المعرّف يُدمج في نص الاستعلام مباشرةً — حقن SQL.
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindValue(':driverID', $driverID);
|
||||
$stmt->execute();
|
||||
try {
|
||||
$stmt = $primary->prepare($sql);
|
||||
$stmt->bindValue(':driverID', $driverID);
|
||||
$stmt->execute();
|
||||
} catch (PDOException $e) {
|
||||
// بلا هذا الالتقاط كان أي خلل في القاعدة يصير 500 يُسقط الشاشة كلها.
|
||||
error_log('[driverStatistic] ' . $e->getMessage());
|
||||
printFailure("Statistics unavailable");
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// Fetch the record
|
||||
$row = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
printSuccess( $row);
|
||||
|
||||
|
||||
}
|
||||
else{
|
||||
// Print a failure message
|
||||
printFailure($message = "No wallet record found");
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* income_summary_s2s.php — ملخّص دخل السائق المارّ بالمحفظة
|
||||
* ─────────────────────────────────────────────────────────────
|
||||
* يجيب على سؤال واحد: هل قناة تحصيل هذا السائق حيّة؟
|
||||
*
|
||||
* تستعمله بوابة الائتمان في محرك الالتزامات. سائق الكاش الذي يحصّل
|
||||
* نقداً ولا تمرّ أرباحه بالمحفظة يظهر بأرباح صفر كل يوم، فسقف الخصم
|
||||
* اليومي صفر، فلا يُحصَّل منه شيء أبداً — ومنحه وقوداً أو صيانة بالدَّين
|
||||
* يعني تسليمه قيمةً فعلية مقابل وعد بالسداد من قناة لا يمرّ بها ماله.
|
||||
*
|
||||
* المقياس الأساسي أيام لا مبلغ: السؤال ليس «كم يكسب؟» بل «هل المال
|
||||
* يمرّ من هنا بانتظام؟». دخل واحد كبير قد يكون استرداداً أو تسوية
|
||||
* حادثة؛ اثنا عشر يوماً متفرّقاً قناةٌ تعمل.
|
||||
*
|
||||
* قراءة فقط — لا تكتب شيئاً ولا تلمس رصيداً.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../../jwtconnect.php';
|
||||
|
||||
// نداء خادم-لخادم حصراً. الرد يُبنى عليه قرار منح ائتمان، فلا يُفتح
|
||||
// لجلسة سائق قد تسأل عن غيره.
|
||||
$providedKey = $_SERVER['HTTP_X_S2S_API_KEY'] ?? '';
|
||||
$expectedKey = getenv('S2S_SHARED_KEY') ?: '';
|
||||
if (empty($expectedKey) || empty($providedKey) || !hash_equals($expectedKey, $providedKey)) {
|
||||
http_response_code(401);
|
||||
printFailure('Unauthorized: Invalid or missing X-S2S-Api-Key.');
|
||||
exit;
|
||||
}
|
||||
|
||||
$driverID = filterRequest('driverID');
|
||||
$windowDays = (int) (filterRequest('window_days', 'numeric') ?: 30);
|
||||
|
||||
if (empty($driverID)) {
|
||||
printFailure('Missing required parameter: driverID');
|
||||
exit;
|
||||
}
|
||||
|
||||
// نافذة محدودة: مسح سنوات من الحركات لكل فحص أهلية حِمل بلا فائدة —
|
||||
// دخلُ سائقٍ قبل ستة أشهر لا يقول شيئاً عن قناته اليوم.
|
||||
if ($windowDays < 1 || $windowDays > 180) {
|
||||
$windowDays = 30;
|
||||
}
|
||||
|
||||
try {
|
||||
// الصفوف الموجبة وحدها. السالبة خصومات (عمولة، التزامات) وعدّها
|
||||
// هنا كان سيجعل سائقاً يُخصم منه كثيراً يبدو ذا قناة نشطة.
|
||||
//
|
||||
// وباستثناء صفوف الالتزامات صراحةً: 'obligation' مرجعها هذا المحرك
|
||||
// نفسه، ولو ظهرت يوماً بمبلغ موجب (تصحيح، استرداد) لصارت البوابة
|
||||
// تقيس أثر نفسها.
|
||||
$st = $con->prepare("
|
||||
SELECT
|
||||
COUNT(DISTINCT DATE(dateCreated)) AS days_with_income,
|
||||
COALESCE(SUM(amount), 0) AS total_income,
|
||||
MAX(DATE(dateCreated)) AS last_income_date
|
||||
FROM driverWallet
|
||||
WHERE driverID = ?
|
||||
AND amount > 0
|
||||
AND paymentMethod <> 'obligation'
|
||||
AND dateCreated >= DATE_SUB(CURDATE(), INTERVAL ? DAY)
|
||||
");
|
||||
$st->execute([$driverID, $windowDays]);
|
||||
$row = $st->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
printSuccess([
|
||||
'driver_id' => $driverID,
|
||||
'window_days' => $windowDays,
|
||||
'days_with_income' => (int) ($row['days_with_income'] ?? 0),
|
||||
'total_income' => (float) ($row['total_income'] ?? 0),
|
||||
'last_income_date' => $row['last_income_date'] ?? null,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
error_log('[obligation/income_summary] ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
printFailure('Failed to read income summary');
|
||||
}
|
||||
@@ -3,13 +3,13 @@ include "../../connect.php";
|
||||
$passenger_id = filterRequest("passenger_id");
|
||||
|
||||
$sql = "SELECT
|
||||
COALESCE(dummy.passenger_id, '$passenger_id') AS passenger_id,
|
||||
COALESCE(SUM(pw.balance), 0) AS total, -- Adjust column to represent payments
|
||||
COALESCE(dummy.passenger_id, :passenger_id) AS passenger_id,
|
||||
COALESCE(SUM(pw.balance), 0) AS total,
|
||||
COALESCE(p.first_name, '') AS first_name,
|
||||
COALESCE(p.last_name, '') AS last_name,
|
||||
COALESCE(p.phone, '') AS phone
|
||||
FROM
|
||||
(SELECT '$passenger_id' AS passenger_id) AS dummy
|
||||
(SELECT :passenger_id2 AS passenger_id) AS dummy
|
||||
LEFT JOIN `passengerWallet` pw ON pw.passenger_id = dummy.passenger_id
|
||||
LEFT JOIN passengers p ON p.id = dummy.passenger_id
|
||||
GROUP BY
|
||||
@@ -17,17 +17,13 @@ GROUP BY
|
||||
LIMIT 0, 25;
|
||||
";
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute();
|
||||
$stmt->execute(['passenger_id' => $passenger_id, 'passenger_id2' => $passenger_id]);
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// Fetch the record
|
||||
$row = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
printSuccess( $row);
|
||||
|
||||
}
|
||||
else{
|
||||
// Print a failure message
|
||||
printFailure($message = "No wallet record found");
|
||||
// LEFT JOIN يُرجع دائماً صفاً واحداً على الأقل (balance=0 إذا لم توجد محفظة)
|
||||
$row = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if (!empty($row)) {
|
||||
printSuccess($row);
|
||||
} else {
|
||||
printFailure("No wallet record found");
|
||||
}
|
||||
?>
|
||||
@@ -1,70 +1,71 @@
|
||||
<?php
|
||||
include "../../connect.php";
|
||||
include_once "../../db_primary.php";
|
||||
|
||||
$driverID = filterRequest("driverID");
|
||||
|
||||
$sql = "SELECT
|
||||
(
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
`ride`
|
||||
WHERE
|
||||
LOWER(`ride`.`status`) IN ('finished','completed')
|
||||
AND `ride`.`created_at` BETWEEN CURRENT_DATE() + INTERVAL 7 HOUR AND CURRENT_DATE() + INTERVAL 10 HOUR
|
||||
AND `ride`.`driver_id` = :driverID
|
||||
) AS morning_count,
|
||||
(
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
`ride`
|
||||
WHERE
|
||||
LOWER(`ride`.`status`) IN ('finished','completed')
|
||||
AND `ride`.`created_at` BETWEEN CURRENT_DATE() + INTERVAL 15 HOUR AND CURRENT_DATE() + INTERVAL 18 HOUR
|
||||
AND `ride`.`driver_id` = :driverID
|
||||
) AS afternoon_count,
|
||||
(
|
||||
SELECT
|
||||
COALESCE(SUM(amount), 0) AS total_amount
|
||||
FROM
|
||||
payments
|
||||
WHERE
|
||||
isGiven = 'waiting' AND `driverID` = :driverID
|
||||
) AS total_amount,
|
||||
(
|
||||
SELECT
|
||||
COALESCE(SUM(price), 0) AS total_amount
|
||||
FROM
|
||||
ride
|
||||
WHERE
|
||||
`driver_id` = :driverID
|
||||
AND LOWER(`ride`.`status`) IN ('finished','completed')
|
||||
AND `ride`.`created_at` > CURRENT_DATE() - INTERVAL 1 WEEK
|
||||
) AS total_amount_last_week
|
||||
FROM
|
||||
dual
|
||||
LIMIT 1;
|
||||
|
||||
|
||||
";
|
||||
/**
|
||||
* كان معرّف السائق يُدمج في نص الاستعلام مباشرةً (حقن SQL)، وكانت الحالة
|
||||
* تُطابق 'Finished' فقط بينما خط الرحلات الحالي يكتب 'completed' — فتظهر
|
||||
* أرباح السائق ورحلاته أصفاراً.
|
||||
* كان استعلاماً واحداً يقرأ `payments` و`ride` معاً — و`ride` ليس في قاعدة
|
||||
* الدفع. النتيجة `SQLSTATE[42S02]: Table 'payment.ride' doesn't exist`، وبلا
|
||||
* try/catch صار خطأً قاتلاً يُسقط النقطة بـ 500 فتعلق شاشة رصيد الكابتن.
|
||||
* (وقبل إطفاء display_errors كان الخطأ يُطبع داخل الردّ فيصل التطبيق كـ
|
||||
* FormatException على `<br />` — نفس العلّة بوجه آخر.)
|
||||
*
|
||||
* الفصل المعتمد: الرصيد أولاً ومستقلاً، والإحصاءات ثانياً وقابلة للفشل.
|
||||
* الرصيد رقم مالي من `payments` في قاعدة الدفع ولا عذر لسقوطه، أما عدّ
|
||||
* الرحلات فمعلومة مساعدة من قاعدة أخرى — تعذّرها يُعيد أصفاراً ولا يحجب
|
||||
* عن الكابتن رصيده.
|
||||
*
|
||||
* و`ride` تُقرأ عبر اتصال مستقل لا عبر إشارة مؤهَّلة: القاعدتان قد تكونان
|
||||
* على خادمين مختلفين — انظر db_primary.php.
|
||||
*/
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->bindValue(':driverID', $driverID);
|
||||
$stmt->execute();
|
||||
|
||||
if ($stmt->rowCount() > 0) {
|
||||
// Fetch the record
|
||||
$row = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
// ── ١. الرصيد — من قاعدة الدفع، لا يعتمد على شيء خارجها ──
|
||||
$balanceStmt = $con->prepare(
|
||||
"SELECT COALESCE(SUM(amount), 0) AS total_amount
|
||||
FROM payments
|
||||
WHERE isGiven = 'waiting' AND `driverID` = :driverID"
|
||||
);
|
||||
$balanceStmt->bindValue(':driverID', $driverID);
|
||||
$balanceStmt->execute();
|
||||
$totalAmount = $balanceStmt->fetchColumn();
|
||||
|
||||
printSuccess( $row);
|
||||
|
||||
$result = [
|
||||
'total_amount' => $totalAmount !== false ? $totalAmount : 0,
|
||||
'morning_count' => 0,
|
||||
'afternoon_count' => 0,
|
||||
'total_amount_last_week' => 0,
|
||||
];
|
||||
|
||||
// ── ٢. إحصاءات الرحلات — اتصال مستقل بالقاعدة الأساسية ──
|
||||
$primary = primaryDbConnection();
|
||||
|
||||
if ($primary !== null) {
|
||||
try {
|
||||
$statsStmt = $primary->prepare(
|
||||
"SELECT
|
||||
COALESCE(SUM(`created_at` BETWEEN CURRENT_DATE() + INTERVAL 7 HOUR
|
||||
AND CURRENT_DATE() + INTERVAL 10 HOUR), 0) AS morning_count,
|
||||
COALESCE(SUM(`created_at` BETWEEN CURRENT_DATE() + INTERVAL 15 HOUR
|
||||
AND CURRENT_DATE() + INTERVAL 18 HOUR), 0) AS afternoon_count,
|
||||
COALESCE(SUM(CASE WHEN `created_at` > CURRENT_DATE() - INTERVAL 1 WEEK
|
||||
THEN `price` ELSE 0 END), 0) AS total_amount_last_week
|
||||
FROM `ride`
|
||||
WHERE `driver_id` = :driverID
|
||||
AND LOWER(`status`) IN ('finished','completed')"
|
||||
);
|
||||
$statsStmt->bindValue(':driverID', $driverID);
|
||||
$statsStmt->execute();
|
||||
|
||||
if ($stats = $statsStmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
$result['morning_count'] = (int) $stats['morning_count'];
|
||||
$result['afternoon_count'] = (int) $stats['afternoon_count'];
|
||||
$result['total_amount_last_week'] = $stats['total_amount_last_week'];
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log('[getAllPayment] ride stats unavailable: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
else{
|
||||
// Print a failure message
|
||||
printFailure($message = "No wallet record found");
|
||||
}
|
||||
?>
|
||||
|
||||
// الشكل كما كان: مصفوفة من صفّ واحد، والتطبيق يقرأ message[0]['total_amount'].
|
||||
printSuccess([$result]);
|
||||
|
||||
@@ -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