api_url = defined('GRAB_API_URL') ? GRAB_API_URL : ''; $this->client_id = defined('GRAB_CLIENT_ID') ? GRAB_CLIENT_ID : ''; $this->client_secret = defined('GRAB_CLIENT_SECRET') ? GRAB_CLIENT_SECRET : ''; helper("general"); $this->outlet = new Outlet(); // Get access token $this->getAccessToken(); } /** * Get OAuth 2.0 access token for GrabExpress API */ private function getAccessToken() { $token_url = $this->api_url . '/grabid/v1/oauth2/token'; $request_data = [ 'client_id' => $this->client_id, 'client_secret' => $this->client_secret, 'grant_type' => 'client_credentials', 'scope' => 'grab_express.partner_deliveries' ]; $headers = [ 'Cache-Control: no-cache', 'Content-Type: application/json' ]; $response = send_api_request('POST', $token_url, $headers, json_encode($request_data)); if (isset($response['access_token'])) { $this->access_token = $response['access_token']; } else { log_message('error', 'Failed to get Grab access token: ' . json_encode($response)); } } /** * Request delivery quotation from GrabExpress */ public function requestQuotation($selected_date = null, $selected_time = null, $total_amount = null, $outlet_id = null, $latitude = null, $longitude = null, $address = null) { // Determine service type based on amount if ($total_amount > 150) { $service_type = 'CAR'; } else { $service_type = 'MOTORCYCLE'; } // Validate service type if (!in_array($service_type, ['MOTORCYCLE', 'CAR'])) { $service_type = 'MOTORCYCLE'; // Default to motorcycle } // Get outlet details $outlet_data = $this->outlet->where('id', $outlet_id)->first(); $outlet_address = $outlet_data['address']; $outlet_latitude = $outlet_data['latitude']; $outlet_longitude = $outlet_data['longitude']; // Validate coordinates if (empty($outlet_latitude) || empty($outlet_longitude) || empty($latitude) || empty($longitude)) { return [ 'error' => 'Invalid coordinates provided', 'outlet_coords' => ['lat' => $outlet_latitude, 'lng' => $outlet_longitude], 'customer_coords' => ['lat' => $latitude, 'lng' => $longitude] ]; } // Prepare request data following GrabExpress API structure $request_data = [ 'serviceType' => 'INSTANT', 'vehicleType' => $service_type === 'CAR' ? 'CAR' : 'BIKE', 'codType' => 'REGULAR', 'packages' => [ [ 'name' => 'Food Package', 'description' => 'Food delivery package', 'quantity' => 1, 'price' => $total_amount, 'dimensions' => [ 'height' => 0, 'width' => 0, 'depth' => 0, 'weight' => 0 ] ] ], 'origin' => [ 'address' => $outlet_address, 'keywords' => 'US Pizza Outlet', 'cityCode' => 'KUL', // Kuala Lumpur, Malaysia 'coordinates' => [ 'latitude' => (float) $outlet_latitude, 'longitude' => (float) $outlet_longitude ] ], 'destination' => [ 'address' => $address, 'keywords' => 'Customer Address', 'cityCode' => 'KUL', // Kuala Lumpur, Malaysia 'coordinates' => [ 'latitude' => (float) $latitude, 'longitude' => (float) $longitude ] ] ]; // Handle preorder/scheduled delivery if ($selected_date && $selected_time) { $local_datetime = $selected_date . ' ' . $selected_time; $dt = new \DateTime($local_datetime, new \DateTimeZone('Asia/Kuala_Lumpur')); $request_data['schedule'] = [ 'pickupTimeFrom' => $dt->format('Y-m-d\TH:i:s+08:00'), 'pickupTimeTo' => $dt->add(new \DateInterval('PT1H'))->format('Y-m-d\TH:i:s+08:00') ]; } $headers = [ 'Authorization: Bearer ' . $this->access_token, 'Content-Type: application/json', 'cache-control: no-cache' ]; $response = send_api_request('POST', $this->api_url . '/grab-express-sandbox/v1/deliveries/quotes', $headers, json_encode($request_data)); // Debug: Log the response log_message('info', 'Grab Quotation Response: ' . json_encode($response)); // Log the request $log_grab = new LogGrab(); $log_grab->insert([ 'url' => $this->api_url . '/grab-express-sandbox/v1/deliveries/quotes', 'request' => json_encode($request_data), 'respond' => json_encode($response), 'quotation_id' => $response['quotes'][0]['quoteId'] ?? null, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]); return $response; } /** * Submit delivery order to GrabExpress */ public function submitOrder($order_id) { $orders = new Orders(); $order = $orders->select('orders.*, customer_addresses.address, customer_addresses.latitude, customer_addresses.longitude, customer_addresses.name as recipient_name, customer_addresses.phone as recipient_phone, customer_addresses.note as recipient_note, outlets.pic_name, outlets.pic_phone') ->join('outlets', 'outlets.id = orders.outlet_id') ->join('customer_addresses', 'customer_addresses.id = orders.customer_address_id AND customer_addresses.deleted_at IS NULL', 'left') ->where('orders.id', $order_id) ->where('orders.deleted_at', null) ->first(); $quotation_id = $order['grab_quot_id']; // Get outlet data for origin coordinates $outlet_data = $this->outlet->where('id', $order['outlet_id'])->first(); if (!$outlet_data) { return ['error' => 'Outlet not found']; } $request_data = [ 'merchantOrderID' => 'USPIZZA_' . $order_id, 'serviceType' => 'INSTANT', 'vehicleType' => 'BIKE', 'codType' => 'REGULAR', 'paymentMethod' => 'CASHLESS', 'packages' => [ [ 'name' => 'Food Package', 'description' => 'Food delivery package', 'quantity' => 1, 'price' => (int) (($order['grand_total'] ?? 0)), 'dimensions' => [ 'height' => 1, 'width' => 1, 'depth' => 1, 'weight' => 1 ] ] ], 'origin' => [ 'address' => $outlet_data['address'], 'cityCode' => 'KUL', 'coordinates' => [ 'latitude' => round((float) $outlet_data['latitude'], 6), 'longitude' => round((float) $outlet_data['longitude'], 6) ] ], 'destination' => [ 'address' => $order['address'], 'cityCode' => 'KUL', 'coordinates' => [ 'latitude' => round((float) $order['latitude'], 6), 'longitude' => round((float) $order['longitude'], 6) ] ], 'recipient' => [ 'firstName' => $order['recipient_name'] ?? 'Customer', 'lastName' => '', 'email' => 'customer@example.com', 'phone' => $order['recipient_phone'] ? $order['recipient_phone'] : '0123456789', 'smsEnabled' => true ], 'sender' => [ 'firstName' => $outlet_data['pic_name'] ?? 'US Pizza', 'companyName' => 'US Pizza', 'email' => 'delivery@uspizza.com', 'phone' => $outlet_data['pic_phone'] ? $outlet_data['pic_phone'] : '0123456789', 'smsEnabled' => true ] ]; // Add schedule if it's a preorder if ($order['selected_date'] && $order['selected_time']) { $local_datetime = $order['selected_date'] . ' ' . $order['selected_time']; $dt = new \DateTime($local_datetime, new \DateTimeZone('Asia/Kuala_Lumpur')); $request_data['schedule'] = [ 'pickupTimeFrom' => $dt->format('Y-m-d\TH:i:s+08:00'), 'pickupTimeTo' => $dt->add(new \DateInterval('PT1H'))->format('Y-m-d\TH:i:s+08:00') ]; } // Remove schedule temporarily to test if that's causing the issue // unset($request_data['schedule']); // Validate required fields if (empty($request_data['origin']['address']) || empty($request_data['destination']['address'])) { return ['error' => 'Missing address information']; } if (empty($request_data['origin']['coordinates']['latitude']) || empty($request_data['origin']['coordinates']['longitude']) || empty($request_data['destination']['coordinates']['latitude']) || empty($request_data['destination']['coordinates']['longitude'])) { return ['error' => 'Missing coordinate information']; } $headers = [ 'Authorization: Bearer ' . $this->access_token, 'Content-Type: application/json; charset=utf-8', 'cache-control: no-cache' ]; log_message('info', 'Grab Submit Order Headers: ' . json_encode($headers)); log_message('info', 'Grab Submit Order Request: ' . json_encode($request_data)); // Debug: Check JSON encoding $json_request = json_encode($request_data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if (json_last_error() !== JSON_ERROR_NONE) { log_message('error', 'JSON encoding error: ' . json_last_error_msg()); return ['error' => 'JSON encoding failed: ' . json_last_error_msg()]; } log_message('info', 'Grab Submit Order JSON: ' . $json_request); $response = send_api_request('POST', $this->api_url . '/grab-express-sandbox/v1/deliveries', $headers, $json_request); // Debug: Log the response log_message('info', 'Grab Submit Order Response: ' . json_encode($response)); // Log the request $log_grab = new LogGrab(); $log_grab->insert([ 'url' => $this->api_url . '/grab-express-sandbox/v1/deliveries', 'request' => json_encode($request_data), 'respond' => json_encode($response), 'quotation_id' => $quotation_id, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]); return $response; } /** * Handle webhook notifications from GrabExpress */ public function handleWebhook($data) { // Log the webhook $log_grab = new LogGrab(); $log_grab->insert([ 'url' => $this->api_url . '/webhook', 'request' => json_encode($data), 'respond' => 'Status: ' . ($data['status'] ?? 'Unknown'), 'quotation_id' => null, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]); // Handle delivery status updates if (isset($data['deliveryID']) && isset($data['status'])) { $delivery_id = $data['deliveryID']; $status = $data['status']; $merchant_order_id = $data['merchantOrderID'] ?? null; $tracking_url = $data['trackURL'] ?? null; $pod_url = $data['dropoffProofURL'] ?? null; // Update order status $this->updateOrderStatus($delivery_id, $status, $merchant_order_id, $tracking_url, $pod_url); // Log additional webhook data log_message('info', 'Grab Webhook - Delivery ID: ' . $delivery_id . ', Status: ' . $status . ', Order: ' . $merchant_order_id); // Log driver information if available if (isset($data['driver'])) { log_message('info', 'Grab Webhook - Driver: ' . $data['driver']['name'] . ', Phone: ' . $data['driver']['phone'] . ', License: ' . $data['driver']['licensePlate']); } // Log pickup pin if available if (isset($data['pickupPin'])) { log_message('info', 'Grab Webhook - Pickup Pin: ' . $data['pickupPin']); } } return true; } /** * Update order status based on delivery status */ private function updateOrderStatus($delivery_id, $status, $merchant_order_id = null, $tracking_url = null, $pod_url = null) { $orders = new Orders(); // Try to find order by delivery ID first, then by merchant order ID $order = null; if ($delivery_id) { $order = $orders->select('orders.*, customer_addresses.phone as recipient_phone') ->join('order_deliveries', 'order_deliveries.order_id = orders.id') ->join('customer_addresses', 'customer_addresses.id = orders.customer_address_id AND customer_addresses.deleted_at IS NULL', 'left') ->where('order_deliveries.provider_name', 'Grab') ->where('order_deliveries.provider_order_id', $delivery_id) ->first(); } // If not found by delivery ID, try by merchant order ID if (!$order && $merchant_order_id) { $order_id = str_replace('USPIZZA_', '', $merchant_order_id); $order = $orders->where('id', $order_id)->first(); } // echo(123);exit; if ($order) { $order_id = $order['id']; // echo($order_id); // exit; $order_status = 'pending'; // Map GrabExpress status to internal status switch ($status) { case 'PICKING_UP': $order_status = 'picked_up'; break; case 'IN_DELIVERY': $order_status = 'on_the_way'; break; case 'COMPLETED': $order_status = 'completed'; break; case 'CANCELLED': $order_status = 'cancelled'; break; default: log_message('info', 'Grab Webhook - Unknown status: ' . $status); $order_status = 'pending'; break; } // Update order status $orders->update($order_id, ['status' => $order_status]); //send notification to customer $message = ''; switch($order_status){ case 'on_the_way': $message = "🚚 Your order [**".$order['order_so']."**] is on the way!\nThe driver is delivering your food now.\nPlease get ready to receive it.\nHere is the tracking link: \n\n" . $tracking_url; break; case 'completed': $message = "🎉 Your order [**".$order['order_so']."**] has been completed.\nWe hope you enjoy your meal!\nThank you for ordering with us."; break; } if($message){ $wato = new Wato(); $wato->pushNotification($order['recipient_phone'], $message); } log_message('info', 'Grab Webhook - Updated order ' . $order_id . ' status to: ' . $order_status); // Update delivery record with additional information $order_deliveries = new OrderDeliveries(); $delivery_update_data = [ 'status' => $order_status, 'updated_at' => date('Y-m-d H:i:s'), 'tracking_link' => $tracking_url, 'POD_url' => $pod_url ]; // Add delivery completion timestamp if completed if ($order_status === 'completed') { $delivery_update_data['delivered_at'] = date('Y-m-d H:i:s'); } // Update delivery record $order_deliveries->where('order_id', $order_id) ->where('provider_name', 'Grab') ->where('provider_order_id', $delivery_id) ->set($delivery_update_data) ->update(); } else { log_message('warning', 'Grab Webhook - Order not found for delivery ID: ' . $delivery_id . ' or merchant order ID: ' . $merchant_order_id); } } /** * Get delivery details from GrabExpress */ public function getDeliveryDetails($delivery_id) { $headers = [ 'Authorization: Bearer ' . $this->access_token, 'Content-Type: application/json', 'cache-control: no-cache' ]; $response = send_api_request('GET', $this->api_url . '/grab-express-sandbox/v1/deliveries/' . $delivery_id, $headers); return $response; } /** * Cancel delivery request */ public function cancelDelivery($delivery_id) { $headers = [ 'Authorization: Bearer ' . $this->access_token, 'Content-Type: application/json', 'cache-control: no-cache' ]; $response = send_api_request('DELETE', $this->api_url . '/grab-express-sandbox/v1/deliveries/' . $delivery_id, $headers); // Log the cancellation $log_grab = new LogGrab(); $log_grab->insert([ 'url' => $this->api_url . '/grab-express-sandbox/v1/deliveries/' . $delivery_id, 'request' => 'DELETE request', 'respond' => json_encode($response), 'quotation_id' => null, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]); return $response; } /** * Store additional webhook information like driver details, pickup pin, etc. */ private function storeWebhookInfo($delivery_id, $webhook_data) { $order_deliveries = new OrderDeliveries(); // Find the delivery record $delivery_record = $order_deliveries->where('provider_name', 'Grab') ->where('provider_order_id', $delivery_id) ->first(); if ($delivery_record) { $update_data = []; // Store driver information if available if (isset($webhook_data['driver'])) { $update_data['driver_name'] = $webhook_data['driver']['name'] ?? ''; $update_data['driver_phone'] = $webhook_data['driver']['phone'] ?? ''; $update_data['driver_license'] = $webhook_data['driver']['licensePlate'] ?? ''; } // Store pickup pin if available if (isset($webhook_data['pickupPin'])) { $update_data['pickup_pin'] = $webhook_data['pickupPin']; } // Store tracking URL if available if (isset($webhook_data['trackURL'])) { $update_data['tracking_link'] = $webhook_data['trackURL']; } // Store distance if available if (isset($webhook_data['distance'])) { $update_data['distance_meters'] = $webhook_data['distance']; } // Store country if available if (isset($webhook_data['country'])) { $update_data['country'] = $webhook_data['country']; } // Update the delivery record with additional information if (!empty($update_data)) { $update_data['updated_at'] = date('Y-m-d H:i:s'); $order_deliveries->where('id', $delivery_record['id']) ->set($update_data) ->update(); } } } }