Update: 2026-08-02 17:52:28
This commit is contained in:
@@ -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']);
|
||||
@@ -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');
|
||||
@@ -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]);
|
||||
@@ -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]);
|
||||
@@ -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]);
|
||||
Reference in New Issue
Block a user