Update: 2026-08-02 17:52:28

This commit is contained in:
Hamza-Ayed
2026-08-02 17:52:28 +03:00
parent b78a6797d5
commit 4620e84d34
96 changed files with 6250 additions and 476 deletions
+28
View File
@@ -0,0 +1,28 @@
<?php
// food/order/cancel.php — إلغاء الزبون قبل قبول المطعم فقط (بعده يتطلب اتصالاً بالمطعم)
require_once __DIR__ . '/../connect_app.php';
$orderId = filterRequest('order_id', 'int');
$reason = filterRequest('reason');
if (!$orderId) jsonError('order_id is required');
$order = foodAssertOrderOwnership($orderId, 'customer', $food_passenger_id);
if ($order['status'] !== 'pending') {
jsonError('Order can no longer be cancelled directly — it has already been accepted by the merchant', 409);
}
food_transition_status($orderId, 'cancelled_by_customer', 'customer', $food_passenger_id, $reason);
if ($order['payment_method'] === 'wallet') {
$refunded = foodWalletMove(
$food_passenger_id, (int)$order['grand_total'], 'add',
"food-refund-{$orderId}", "Food order #{$orderId} cancelled"
);
$food_con->prepare(
"INSERT INTO food_order_payments (order_id, type, amount, status) VALUES (?,'release',?,?)"
)->execute([$orderId, (int)$order['grand_total'], $refunded ? 'success' : 'failed']);
if (!$refunded) appLog("[FOOD][ORDER][cancel] refund FAILED for order $orderId — needs manual reconciliation", 'ERROR');
}
jsonSuccess(['order_id' => $orderId, 'status' => 'cancelled_by_customer']);
+113
View File
@@ -0,0 +1,113 @@
<?php
// food/order/create.php — إنشاء الطلب من عرض سعر موقّع + خصم فوري من المحفظة
// body: quote_token, client_order_uuid, delivery_address, delivery_lat, delivery_lng,
// payment_method (wallet|cash), customer_note
require_once __DIR__ . '/../connect_app.php';
requireFoodFields(['quote_token', 'client_order_uuid', 'delivery_address', 'delivery_lat', 'delivery_lng']);
$quoteToken = filterRequest('quote_token');
$clientUuid = filterRequest('client_order_uuid');
$address = filterRequest('delivery_address');
$lat = filterRequest('delivery_lat', 'float');
$lng = filterRequest('delivery_lng', 'float');
$paymentMethod = filterRequest('payment_method') ?: 'wallet';
$note = filterRequest('customer_note');
if (!in_array($paymentMethod, ['wallet', 'cash'], true)) jsonError('Invalid payment_method');
if (!preg_match('/^[0-9a-fA-F-]{36}$/', $clientUuid)) jsonError('client_order_uuid must be a valid UUID');
$quote = foodVerifyQuote($quoteToken);
if ((string)$quote['passenger_id'] !== $food_passenger_id) jsonError('Quote does not belong to this session', 403);
// idempotency — الضغط المزدوج أو إعادة محاولة الشبكة يرجع نفس الطلب لا طلباً جديداً
$dupSt = $food_con->prepare("SELECT id FROM food_orders WHERE client_order_uuid=? LIMIT 1");
$dupSt->execute([$clientUuid]);
if ($existing = $dupSt->fetch()) {
jsonSuccess(['order_id' => (int)$existing['id'], 'idempotent_replay' => true], 'Order already exists');
}
$maxActive = (int)(getenv('FOOD_MAX_ACTIVE_ORDERS_PER_USER') ?: 3);
$activeSt = $food_con->prepare(
"SELECT COUNT(*) c FROM food_orders WHERE passenger_id=? AND status NOT IN
('delivered','rejected','cancelled_by_customer','cancelled_by_merchant','cancelled_system')"
);
$activeSt->execute([$food_passenger_id]);
if ((int)$activeSt->fetch()['c'] >= $maxActive) {
jsonError("You already have $maxActive active orders — finish or cancel one first", 409);
}
$merchantSt = $food_con->prepare("SELECT id, status, commission_percent FROM food_merchants WHERE id=? LIMIT 1");
$merchantSt->execute([$quote['merchant_id']]);
$merchant = $merchantSt->fetch();
if (!$merchant || $merchant['status'] !== 'active') jsonError('Merchant is no longer available', 409);
$itemsTotal = (int)$quote['items_total'];
$deliveryFee = (int)$quote['delivery_fee'];
$serviceFee = (int)$quote['service_fee'];
$grandTotal = (int)$quote['grand_total'];
$commission = foodComputeCommission($itemsTotal, (float)$merchant['commission_percent']);
if ($paymentMethod === 'wallet') {
$balance = foodWalletGetBalance($food_passenger_id);
if ($balance === null) jsonError('Unable to verify wallet balance. Please try again.', 503);
if ($balance < foodSmallestUnitToDecimal($grandTotal)) {
jsonError('Insufficient wallet balance', 402, ['current_balance' => $balance]);
}
}
$food_con->beginTransaction();
try {
$food_con->prepare(
"INSERT INTO food_orders
(client_order_uuid, passenger_id, merchant_id, status, items_total, delivery_fee, service_fee,
discount, grand_total, commission_amount, payment_method, delivery_address, delivery_lat,
delivery_lng, customer_note)
VALUES (?,?,?,'pending',?,?,?,0,?,?,?,?,?,?,?)"
)->execute([
$clientUuid, $food_passenger_id, $quote['merchant_id'], $itemsTotal, $deliveryFee, $serviceFee,
$grandTotal, $commission, $paymentMethod, $address, $lat, $lng, $note,
]);
$orderId = (int)$food_con->lastInsertId();
$insertLine = $food_con->prepare(
"INSERT INTO food_order_items (order_id, item_id, name_ar_snapshot, unit_price, quantity, option_price_json, line_total)
VALUES (?,?,?,?,?,?,?)"
);
foreach ($quote['lines'] as $line) {
$insertLine->execute([
$orderId, $line['item_id'], $line['name_ar_snapshot'], $line['unit_price'],
$line['quantity'], json_encode($line['options']), $line['line_total'],
]);
}
$food_con->prepare(
"INSERT INTO food_order_status_log (order_id, from_status, to_status, actor_type, actor_id)
VALUES (?,NULL,'pending','customer',?)"
)->execute([$orderId, $food_passenger_id]);
$food_con->commit();
} catch (Throwable $e) {
$food_con->rollBack();
appLog('[FOOD][ORDER][create] ' . $e->getMessage(), 'ERROR');
jsonError('Failed to create order', 500);
}
if ($paymentMethod === 'wallet') {
$debited = foodWalletMove($food_passenger_id, $grandTotal, 'subtract', "food-order-{$orderId}", "Food order #{$orderId}");
if (!$debited) {
// فشل الخصم بعد إنشاء السجل — نُلغي الطلب فوراً بدل ترك طلب بلا دفع
food_transition_status($orderId, 'cancelled_system', 'system', 'wallet', 'Wallet debit failed');
jsonError('Wallet payment failed — order cancelled', 402);
}
$food_con->prepare(
"INSERT INTO food_order_payments (order_id, type, amount, status) VALUES (?,'hold',?,'success')"
)->execute([$orderId, $grandTotal]);
}
foodPushToSocket('order_status_update', [
'order_id' => $orderId, 'status' => 'pending', 'passenger_id' => $food_passenger_id,
'merchant_id' => (int)$quote['merchant_id'], 'courier_id' => null,
]);
jsonSuccess(['order_id' => $orderId, 'status' => 'pending', 'grand_total' => $grandTotal], 'Order placed');
+20
View File
@@ -0,0 +1,20 @@
<?php
// food/order/history.php — سجل طلبات الزبون
require_once __DIR__ . '/../connect_app.php';
$page = max(1, (int)(filterRequest('page', 'int') ?? 1));
$limit = 20;
$offset = ($page - 1) * $limit;
$st = $food_con->prepare(
"SELECT o.id, o.status, o.grand_total, o.created_at, o.delivered_at, o.rating,
m.name_ar AS merchant_name_ar, m.logo_url AS merchant_logo_url
FROM food_orders o
JOIN food_merchants m ON m.id = o.merchant_id
WHERE o.passenger_id = ?
ORDER BY o.created_at DESC
LIMIT $limit OFFSET $offset"
);
$st->execute([$food_passenger_id]);
jsonSuccess(['orders' => $st->fetchAll(), 'page' => $page]);
+34
View File
@@ -0,0 +1,34 @@
<?php
// food/order/rate.php — تقييم الطلب بعد التسليم (يحدّث متوسط تقييم المطعم)
require_once __DIR__ . '/../connect_app.php';
$orderId = filterRequest('order_id', 'int');
$rating = filterRequest('rating', 'int');
$comment = filterRequest('comment');
if (!$orderId || !$rating || $rating < 1 || $rating > 5) jsonError('order_id and rating (1-5) are required');
$order = foodAssertOrderOwnership($orderId, 'customer', $food_passenger_id);
if ($order['status'] !== 'delivered') jsonError('Only delivered orders can be rated', 409);
if ($order['rating'] !== null) jsonError('Order already rated', 409);
$food_con->beginTransaction();
try {
$food_con->prepare("UPDATE food_orders SET rating=?, rating_comment=? WHERE id=?")
->execute([$rating, $comment, $orderId]);
$food_con->prepare(
"UPDATE food_merchants SET
rating_avg = ((rating_avg * rating_count) + ?) / (rating_count + 1),
rating_count = rating_count + 1
WHERE id=?"
)->execute([$rating, $order['merchant_id']]);
$food_con->commit();
} catch (Throwable $e) {
$food_con->rollBack();
appLog('[FOOD][ORDER][rate] ' . $e->getMessage(), 'ERROR');
jsonError('Failed to save rating', 500);
}
jsonSuccess(['order_id' => $orderId, 'rating' => $rating]);
+15
View File
@@ -0,0 +1,15 @@
<?php
// food/order/status.php — حالة الطلب الحالية (مصدر الحقيقة عند إعادة الاتصال بالسوكيت)
require_once __DIR__ . '/../connect_app.php';
$orderId = filterRequest('order_id', 'int');
if (!$orderId) jsonError('order_id is required');
$order = foodAssertOrderOwnership($orderId, 'customer', $food_passenger_id);
$itemsSt = $food_con->prepare("SELECT name_ar_snapshot, unit_price, quantity, line_total FROM food_order_items WHERE order_id=?");
$itemsSt->execute([$orderId]);
$order['items'] = $itemsSt->fetchAll();
// عناوين/أرقام الزبون تُحجب عن السائق قبل courier_assigned وبعد delivered — هنا هي بوابة الزبون نفسه فلا حجب
jsonSuccess(['order' => $order]);