Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
@@ -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);
}