2025-11-06 13:41:06 +08:00

523 lines
20 KiB
PHP

<?php
namespace App\Libraries;
use App\Models\Outlet;
use App\Models\Orders;
use App\Models\LogGrab;
use App\Models\OrderDeliveries;
class Grab {
protected $api_url;
protected $client_id;
protected $client_secret;
protected $access_token;
protected $outlet;
public function __construct() {
$this->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();
}
}
}
}