Food delivery driver module + masked calls + TURN
This commit is contained in:
@@ -4,8 +4,11 @@ require_once __DIR__ . '/../connect_courier.php';
|
||||
|
||||
$st = $food_con->prepare(
|
||||
"SELECT o.id, o.status, o.merchant_id, m.name_ar AS merchant_name_ar, m.latitude AS merchant_lat,
|
||||
m.longitude AS merchant_lng, m.address AS merchant_address, o.delivery_fee,
|
||||
o.delivery_address, o.delivery_lat, o.delivery_lng, o.created_at,
|
||||
m.longitude AS merchant_lng, m.address AS merchant_address, m.avg_prep_minutes,
|
||||
o.delivery_fee, o.items_total, o.service_fee, o.discount, o.grand_total, o.payment_method,
|
||||
o.customer_note, o.delivery_address, o.delivery_lat, o.delivery_lng,
|
||||
o.created_at, o.ready_at, o.courier_assigned_at, o.picked_up_at,
|
||||
(SELECT COALESCE(SUM(quantity),0) FROM food_order_items WHERE order_id=o.id) AS items_count,
|
||||
CASE WHEN o.status IN ('courier_assigned','picked_up') THEN o.delivery_address ELSE NULL END AS visible_address
|
||||
FROM food_orders o
|
||||
JOIN food_merchants m ON m.id = o.merchant_id
|
||||
@@ -20,6 +23,15 @@ foreach ($orders as &$o) {
|
||||
unset($o['delivery_address']);
|
||||
$o['delivery_address'] = $o['visible_address'];
|
||||
unset($o['visible_address']);
|
||||
|
||||
$o['items_count'] = (int)$o['items_count'];
|
||||
// نقداً: السائق يحصّل grand_total من الزبون ويحتفظ بأجرته (delivery_fee)،
|
||||
// والباقي يبقى ديناً عليه حتى التسوية — نُظهر الرقمين صراحةً في التطبيق
|
||||
// كي لا يجتهد السائق في الحساب على باب الزبون.
|
||||
$o['cash_to_collect'] = $o['payment_method'] === 'cash' ? (int)$o['grand_total'] : 0;
|
||||
$o['courier_owes'] = $o['payment_method'] === 'cash'
|
||||
? (int)$o['grand_total'] - (int)$o['delivery_fee']
|
||||
: 0;
|
||||
}
|
||||
unset($o);
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// food/courier/call_customer.php — السائق يتصل بالزبون عبر قناة مقنّعة
|
||||
// لا رقم هاتف يُعرض لأي طرف: نفتح جلسة WebRTC ونُشعر الزبون بمعرّف الجلسة فقط.
|
||||
require_once __DIR__ . '/../connect_courier.php';
|
||||
|
||||
$orderId = filterRequest('order_id', 'int');
|
||||
if (!$orderId) jsonError('order_id is required');
|
||||
|
||||
$order = foodAssertOrderOwnership($orderId, 'courier', $food_courier_id);
|
||||
|
||||
if (!foodOrderAllowsCall($order)) {
|
||||
// بعد التسليم أو قبل الإسناد لا حاجة تشغيلية للاتصال — والقناة تُقفل
|
||||
jsonError('Calling is only allowed while the delivery is active', 403);
|
||||
}
|
||||
|
||||
if (foodCallQuotaExceeded($orderId, 'courier')) {
|
||||
jsonError('Too many call attempts for this order', 429);
|
||||
}
|
||||
|
||||
$session = foodCreateCallSession($orderId, $food_courier_id, (string)$order['passenger_id']);
|
||||
if (!$session) jsonError('Call service unavailable', 502);
|
||||
|
||||
// إشعار صامت للزبون عبر موضوع FCM الخاص به — نفس القناة المستعملة في وحدة
|
||||
// الطعام أصلاً (ممنوع Database::get('main') هنا لجلب توكن الجهاز).
|
||||
// الاسم المعروض عام عمداً: هوية السائق الحقيقية لا تُكشف للزبون.
|
||||
foodSendNotificationToPassenger(
|
||||
(string)$order['passenger_id'],
|
||||
'مكالمة واردة',
|
||||
'سائق التوصيل يتصل بك',
|
||||
[
|
||||
// تطبيق الراكب يفرز الإشعارات بـ data['category'] — ونُبقي 'type'
|
||||
// للتوافق مع بقية حمولات وحدة الطعام.
|
||||
'category' => 'incoming_call',
|
||||
'type' => 'incoming_call',
|
||||
'session_id' => $session['session_id'],
|
||||
'caller_name' => 'سائق التوصيل',
|
||||
'caller_avatar' => '',
|
||||
'ride_id' => 'food_' . $orderId,
|
||||
'food_order_id' => (string)$orderId,
|
||||
]
|
||||
);
|
||||
|
||||
appLog("[FOOD][CALL] courier {$food_courier_id} → passenger (order {$orderId})", 'INFO');
|
||||
|
||||
jsonSuccess([
|
||||
'session_id' => $session['session_id'],
|
||||
'expires_in' => $session['expires_in'],
|
||||
]);
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
// food/courier/earnings.php — ملخص أرباح التوصيل للسائق (اليوم/الأسبوع/الشهر)
|
||||
// المبالغ من food_orders.delivery_fee للطلبات المسلَّمة فقط — نفس ما يُقيَّد في
|
||||
// food_order_payments (courier_payout)، والتسوية الفعلية خارج نطاق هذا الملف.
|
||||
require_once __DIR__ . '/../connect_courier.php';
|
||||
|
||||
$sums = $food_con->prepare(
|
||||
"SELECT
|
||||
COUNT(*) AS total_orders,
|
||||
COALESCE(SUM(delivery_fee),0) AS total_earnings,
|
||||
COALESCE(SUM(CASE WHEN DATE(delivered_at)=CURDATE() THEN delivery_fee END),0) AS today_earnings,
|
||||
COUNT(CASE WHEN DATE(delivered_at)=CURDATE() THEN 1 END) AS today_orders,
|
||||
COALESCE(SUM(CASE WHEN delivered_at >= (NOW() - INTERVAL 7 DAY) THEN delivery_fee END),0) AS week_earnings,
|
||||
COUNT(CASE WHEN delivered_at >= (NOW() - INTERVAL 7 DAY) THEN 1 END) AS week_orders,
|
||||
COALESCE(SUM(CASE WHEN delivered_at >= (NOW() - INTERVAL 30 DAY) THEN delivery_fee END),0) AS month_earnings,
|
||||
COUNT(CASE WHEN delivered_at >= (NOW() - INTERVAL 30 DAY) THEN 1 END) AS month_orders
|
||||
FROM food_orders
|
||||
WHERE courier_id=? AND status='delivered'"
|
||||
);
|
||||
$sums->execute([$food_courier_id]);
|
||||
$row = $sums->fetch() ?: [];
|
||||
|
||||
// ديون التحصيل النقدي غير المسوّاة — السائق يحتاج يعرف كم عليه قبل ما يفاجأ بخصم
|
||||
$owedSt = $food_con->prepare(
|
||||
"SELECT COALESCE(SUM(p.amount),0) AS owed
|
||||
FROM food_order_payments p
|
||||
JOIN food_orders o ON o.id = p.order_id
|
||||
WHERE o.courier_id=? AND p.type='cash_settlement' AND p.status='pending'"
|
||||
);
|
||||
$owedSt->execute([$food_courier_id]);
|
||||
|
||||
// آخر 7 أيام مفصّلة — للرسم البياني في التطبيق
|
||||
$dailySt = $food_con->prepare(
|
||||
"SELECT DATE(delivered_at) AS day, COUNT(*) AS orders_count, COALESCE(SUM(delivery_fee),0) AS earnings
|
||||
FROM food_orders
|
||||
WHERE courier_id=? AND status='delivered' AND delivered_at >= (NOW() - INTERVAL 7 DAY)
|
||||
GROUP BY DATE(delivered_at)
|
||||
ORDER BY day DESC"
|
||||
);
|
||||
$dailySt->execute([$food_courier_id]);
|
||||
|
||||
jsonSuccess([
|
||||
'today_earnings' => (int)($row['today_earnings'] ?? 0),
|
||||
'today_orders' => (int)($row['today_orders'] ?? 0),
|
||||
'week_earnings' => (int)($row['week_earnings'] ?? 0),
|
||||
'week_orders' => (int)($row['week_orders'] ?? 0),
|
||||
'month_earnings' => (int)($row['month_earnings'] ?? 0),
|
||||
'month_orders' => (int)($row['month_orders'] ?? 0),
|
||||
'total_earnings' => (int)($row['total_earnings'] ?? 0),
|
||||
'total_orders' => (int)($row['total_orders'] ?? 0),
|
||||
'pending_cash_owed' => (int)($owedSt->fetch()['owed'] ?? 0),
|
||||
'daily' => array_map(static fn($d) => [
|
||||
'day' => $d['day'],
|
||||
'orders_count' => (int)$d['orders_count'],
|
||||
'earnings' => (int)$d['earnings'],
|
||||
], $dailySt->fetchAll()),
|
||||
]);
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// food/courier/history.php — سجل طلبات التوصيل المنتهية لهذا السائق
|
||||
require_once __DIR__ . '/../connect_courier.php';
|
||||
|
||||
$limit = (int)(filterRequest('limit', 'int') ?: 20);
|
||||
$offset = (int)(filterRequest('offset', 'int') ?: 0);
|
||||
$limit = max(1, min($limit, 50));
|
||||
$offset = max(0, $offset);
|
||||
|
||||
// العنوان النصّي للزبون لا يُعاد في السجل — انتهت الحاجة التشغيلية إليه بالتسليم،
|
||||
// ونفس قاعدة الحجب المطبَّقة في active.php.
|
||||
$st = $food_con->prepare(
|
||||
"SELECT o.id, o.status, o.merchant_id, m.name_ar AS merchant_name_ar, m.address AS merchant_address,
|
||||
o.delivery_fee, o.grand_total, o.payment_method, o.rating,
|
||||
o.courier_assigned_at, o.picked_up_at, o.delivered_at, o.created_at,
|
||||
(SELECT COALESCE(SUM(quantity),0) FROM food_order_items WHERE order_id=o.id) AS items_count,
|
||||
TIMESTAMPDIFF(MINUTE, o.courier_assigned_at, o.delivered_at) AS duration_minutes
|
||||
FROM food_orders o
|
||||
JOIN food_merchants m ON m.id = o.merchant_id
|
||||
WHERE o.courier_id = ? AND o.status = 'delivered'
|
||||
ORDER BY o.delivered_at DESC
|
||||
LIMIT $limit OFFSET $offset"
|
||||
);
|
||||
$st->execute([$food_courier_id]);
|
||||
|
||||
$orders = array_map(static function (array $o): array {
|
||||
$o['items_count'] = (int)$o['items_count'];
|
||||
$o['delivery_fee'] = (int)$o['delivery_fee'];
|
||||
$o['grand_total'] = (int)$o['grand_total'];
|
||||
$o['duration_minutes'] = $o['duration_minutes'] === null ? null : (int)$o['duration_minutes'];
|
||||
return $o;
|
||||
}, $st->fetchAll());
|
||||
|
||||
jsonSuccess(['orders' => $orders, 'limit' => $limit, 'offset' => $offset]);
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
// food/courier/order_details.php — تفاصيل مهمة توصيل واحدة (شاشة المهمة عند السائق)
|
||||
// أصناف الطلب + تفصيل المبالغ + هاتف المطعم للتواصل أثناء الاستلام
|
||||
require_once __DIR__ . '/../connect_courier.php';
|
||||
|
||||
$orderId = filterRequest('order_id', 'int');
|
||||
if (!$orderId) jsonError('order_id is required');
|
||||
|
||||
// يفشل لو لم يكن الطلب مُسنداً لهذا السائق — لا وصول لطلبات الآخرين
|
||||
foodAssertOrderOwnership($orderId, 'courier', $food_courier_id);
|
||||
|
||||
$st = $food_con->prepare(
|
||||
"SELECT o.id, o.status, o.merchant_id, o.items_total, o.delivery_fee, o.service_fee, o.discount,
|
||||
o.grand_total, o.payment_method, o.customer_note, o.delivery_lat, o.delivery_lng,
|
||||
o.created_at, o.ready_at, o.courier_assigned_at, o.picked_up_at, o.delivered_at,
|
||||
CASE WHEN o.status IN ('courier_assigned','picked_up') THEN o.delivery_address ELSE NULL END AS delivery_address,
|
||||
m.name_ar AS merchant_name_ar, m.address AS merchant_address, m.latitude AS merchant_lat,
|
||||
m.longitude AS merchant_lng, m.avg_prep_minutes
|
||||
FROM food_orders o
|
||||
JOIN food_merchants m ON m.id = o.merchant_id
|
||||
WHERE o.id = ? LIMIT 1"
|
||||
);
|
||||
$st->execute([$orderId]);
|
||||
$order = $st->fetch();
|
||||
if (!$order) jsonError('Order not found', 404);
|
||||
|
||||
$itemsSt = $food_con->prepare(
|
||||
"SELECT name_ar_snapshot, quantity, unit_price, line_total, option_price_json
|
||||
FROM food_order_items WHERE order_id=? ORDER BY id ASC"
|
||||
);
|
||||
$itemsSt->execute([$orderId]);
|
||||
$items = array_map(static function (array $i): array {
|
||||
return [
|
||||
'name_ar' => $i['name_ar_snapshot'],
|
||||
'quantity' => (int)$i['quantity'],
|
||||
'unit_price' => (int)$i['unit_price'],
|
||||
'line_total' => (int)$i['line_total'],
|
||||
'options' => $i['option_price_json'] ? json_decode($i['option_price_json'], true) : null,
|
||||
];
|
||||
}, $itemsSt->fetchAll());
|
||||
|
||||
// هاتف المطعم — يُعاد فقط أثناء المهمة النشطة (قبل التسليم)، لا في السجل
|
||||
$merchantPhone = null;
|
||||
if (in_array($order['status'], ['courier_assigned', 'picked_up'], true)) {
|
||||
$phoneSt = $food_con->prepare(
|
||||
"SELECT phone FROM food_merchant_users
|
||||
WHERE merchant_id=? AND is_active=1 ORDER BY role='owner' DESC, id ASC LIMIT 1"
|
||||
);
|
||||
$phoneSt->execute([$order['merchant_id']]);
|
||||
$merchantPhone = $phoneSt->fetch()['phone'] ?? null;
|
||||
}
|
||||
|
||||
// ملاحظة: لا هاتف للزبون هنا — قاعدة siro_food معزولة ولا تحمل بيانات الراكب،
|
||||
// وممنوع Database::get('main') داخل backend/food/. التواصل مع الزبون يبقى عبر
|
||||
// عنوان التسليم والملاحظة، إلى أن تُضاف قناة اتصال مقنّعة كما في الرحلات.
|
||||
$order['items'] = $items;
|
||||
$order['items_count'] = array_sum(array_column($items, 'quantity'));
|
||||
$order['merchant_phone'] = $merchantPhone;
|
||||
$order['cash_to_collect'] = $order['payment_method'] === 'cash' ? (int)$order['grand_total'] : 0;
|
||||
$order['courier_owes'] = $order['payment_method'] === 'cash'
|
||||
? (int)$order['grand_total'] - (int)$order['delivery_fee']
|
||||
: 0;
|
||||
|
||||
jsonSuccess(['order' => $order]);
|
||||
@@ -5,15 +5,25 @@
|
||||
require_once __DIR__ . '/../connect_courier.php';
|
||||
|
||||
$st = $food_con->prepare(
|
||||
"SELECT a.order_id, a.offered_at, o.merchant_id, m.name_ar AS merchant_name_ar,
|
||||
m.latitude AS merchant_lat, m.longitude AS merchant_lng, o.delivery_fee,
|
||||
o.delivery_lat, o.delivery_lng
|
||||
"SELECT a.order_id, a.offered_at
|
||||
FROM food_courier_assignments a
|
||||
JOIN food_orders o ON o.id = a.order_id
|
||||
JOIN food_merchants m ON m.id = o.merchant_id
|
||||
WHERE a.courier_id = ? AND a.status = 'offered' AND a.offered_at > (NOW() - INTERVAL 20 SECOND)
|
||||
AND o.status = 'ready' AND o.courier_id IS NULL
|
||||
ORDER BY a.offered_at ASC"
|
||||
);
|
||||
$st->execute([$food_courier_id]);
|
||||
|
||||
jsonSuccess(['offers' => $st->fetchAll()]);
|
||||
// نفس حمولة مسار السوكيت حرفياً (foodBuildCourierOfferPayload) — شاشة العرض
|
||||
// عند السائق واحدة، فلا يجوز أن تختلف الحقول حسب المسار الذي وصل منه العرض.
|
||||
$offers = [];
|
||||
foreach ($st->fetchAll() as $row) {
|
||||
$payload = foodBuildCourierOfferPayload((int)$row['order_id']);
|
||||
if (!$payload) continue;
|
||||
$offers[] = array_merge(
|
||||
['order_id' => (int)$row['order_id'], 'offered_at' => $row['offered_at'], 'offer_ttl_seconds' => 20],
|
||||
$payload
|
||||
);
|
||||
}
|
||||
|
||||
jsonSuccess(['offers' => $offers]);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
// food/courier/status.php — حالة السائق في وحدة التوصيل عند فتح التبويب
|
||||
// (وضع التوصيل مخزَّن في Redis لا في التطبيق — بدون هذا النداء يفتح التطبيق
|
||||
// على "مغلق" بينما السائق ما زال فعلياً في food:couriers:opted_in ويستقبل عروضاً)
|
||||
require_once __DIR__ . '/../connect_courier.php';
|
||||
|
||||
$enabled = false;
|
||||
if ($redis) {
|
||||
$enabled = (bool)$redis->sIsMember('food:couriers:opted_in', $food_courier_id);
|
||||
}
|
||||
|
||||
$activeSt = $food_con->prepare(
|
||||
"SELECT COUNT(*) AS c FROM food_orders
|
||||
WHERE courier_id=? AND status IN ('courier_assigned','picked_up')"
|
||||
);
|
||||
$activeSt->execute([$food_courier_id]);
|
||||
$activeCount = (int)($activeSt->fetch()['c'] ?? 0);
|
||||
|
||||
$todaySt = $food_con->prepare(
|
||||
"SELECT COUNT(*) AS orders_count, COALESCE(SUM(delivery_fee),0) AS earnings
|
||||
FROM food_orders
|
||||
WHERE courier_id=? AND status='delivered' AND DATE(delivered_at)=CURDATE()"
|
||||
);
|
||||
$todaySt->execute([$food_courier_id]);
|
||||
$today = $todaySt->fetch();
|
||||
|
||||
jsonSuccess([
|
||||
'delivery_mode_enabled' => $enabled,
|
||||
'active_orders_count' => $activeCount,
|
||||
'today_orders_count' => (int)$today['orders_count'],
|
||||
'today_earnings' => (int)$today['earnings'],
|
||||
]);
|
||||
Reference in New Issue
Block a user