config = config('restaurant-delivery'); $this->initializeFirebase(); } protected function initializeFirebase(): void { if (! $this->config['enabled']) { return; } try { $this->factory = (new Factory) ->withServiceAccount($this->config['credentials_path']) ->withDatabaseUri($this->config['database_url']); } catch (\Exception $e) { Log::error('Firebase initialization failed', [ 'error' => $e->getMessage(), ]); } } public function getDatabase(): ?Database { if (! $this->database && $this->factory) { $this->database = $this->factory->createDatabase(); } return $this->database; } public function getMessaging(): ?Messaging { if (! $this->messaging && $this->factory) { $this->messaging = $this->factory->createMessaging(); } return $this->messaging; } /* |-------------------------------------------------------------------------- | Rider Location Operations |-------------------------------------------------------------------------- */ /** * Update rider's live location in Firebase */ public function updateRiderLocation( int|string $riderId, float $latitude, float $longitude, ?float $speed = null, ?float $bearing = null, ?float $accuracy = null ): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $path = str_replace('{rider_id}', (string) $riderId, $this->config['paths']['riders_location']); $locationData = [ 'lat' => $latitude, 'lng' => $longitude, 'speed' => $speed ?? 0, 'bearing' => $bearing ?? 0, 'accuracy' => $accuracy ?? 0, 'timestamp' => time() * 1000, // JavaScript timestamp 'updated_at' => now()->toIso8601String(), ]; $database->getReference($path)->set($locationData); // Cache locally for quick access Cache::put( "rider_location_{$riderId}", $locationData, config('restaurant-delivery.cache.ttl.rider_location') ); return true; } catch (FirebaseException $e) { Log::error('Failed to update rider location in Firebase', [ 'rider_id' => $riderId, 'error' => $e->getMessage(), ]); return false; } } /** * Get rider's current location from Firebase */ public function getRiderLocation(int|string $riderId): ?array { // Try cache first $cached = Cache::get("rider_location_{$riderId}"); if ($cached) { return $cached; } try { $database = $this->getDatabase(); if (! $database) { return null; } $path = str_replace('{rider_id}', (string) $riderId, $this->config['paths']['riders_location']); $snapshot = $database->getReference($path)->getSnapshot(); return $snapshot->exists() ? $snapshot->getValue() : null; } catch (FirebaseException $e) { Log::error('Failed to get rider location from Firebase', [ 'rider_id' => $riderId, 'error' => $e->getMessage(), ]); return null; } } /** * Check if rider location is stale */ public function isRiderLocationStale(int|string $riderId): bool { $location = $this->getRiderLocation($riderId); if (! $location || ! isset($location['timestamp'])) { return true; } $lastUpdate = (int) ($location['timestamp'] / 1000); // Convert from JS timestamp $staleThreshold = $this->config['location']['stale_threshold']; return (time() - $lastUpdate) > $staleThreshold; } /** * Check if rider is considered offline */ public function isRiderOffline(int|string $riderId): bool { $location = $this->getRiderLocation($riderId); if (! $location || ! isset($location['timestamp'])) { return true; } $lastUpdate = (int) ($location['timestamp'] / 1000); $offlineThreshold = $this->config['location']['offline_threshold']; return (time() - $lastUpdate) > $offlineThreshold; } /* |-------------------------------------------------------------------------- | Rider Status Operations |-------------------------------------------------------------------------- */ /** * Update rider online/offline status */ public function updateRiderStatus( int|string $riderId, string $status, ?array $metadata = null ): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $path = str_replace('{rider_id}', (string) $riderId, $this->config['paths']['rider_status']); $statusData = [ 'status' => $status, // online, offline, busy, on_delivery 'updated_at' => now()->toIso8601String(), 'timestamp' => time() * 1000, ]; if ($metadata) { $statusData = array_merge($statusData, $metadata); } $database->getReference($path)->set($statusData); return true; } catch (FirebaseException $e) { Log::error('Failed to update rider status in Firebase', [ 'rider_id' => $riderId, 'status' => $status, 'error' => $e->getMessage(), ]); return false; } } /** * Get rider's current status */ public function getRiderStatus(int|string $riderId): ?array { try { $database = $this->getDatabase(); if (! $database) { return null; } $path = str_replace('{rider_id}', (string) $riderId, $this->config['paths']['rider_status']); $snapshot = $database->getReference($path)->getSnapshot(); return $snapshot->exists() ? $snapshot->getValue() : null; } catch (FirebaseException $e) { Log::error('Failed to get rider status from Firebase', [ 'rider_id' => $riderId, 'error' => $e->getMessage(), ]); return null; } } /* |-------------------------------------------------------------------------- | Delivery Tracking Operations |-------------------------------------------------------------------------- */ /** * Initialize delivery tracking in Firebase */ public function initializeDeliveryTracking( int|string $deliveryId, array $deliveryData ): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $path = str_replace('{delivery_id}', (string) $deliveryId, $this->config['paths']['delivery_tracking']); $trackingData = [ 'delivery_id' => $deliveryId, 'status' => $deliveryData['status'] ?? 'pending', 'rider_id' => $deliveryData['rider_id'] ?? null, 'restaurant' => [ 'id' => $deliveryData['restaurant_id'] ?? null, 'name' => $deliveryData['restaurant_name'] ?? null, 'lat' => $deliveryData['pickup_latitude'], 'lng' => $deliveryData['pickup_longitude'], 'address' => $deliveryData['pickup_address'] ?? null, ], 'customer' => [ 'lat' => $deliveryData['drop_latitude'], 'lng' => $deliveryData['drop_longitude'], 'address' => $deliveryData['drop_address'] ?? null, ], 'rider_location' => null, 'route' => $deliveryData['route'] ?? null, 'eta' => $deliveryData['eta'] ?? null, 'distance' => $deliveryData['distance'] ?? null, 'created_at' => now()->toIso8601String(), 'updated_at' => now()->toIso8601String(), ]; $database->getReference($path)->set($trackingData); return true; } catch (FirebaseException $e) { Log::error('Failed to initialize delivery tracking in Firebase', [ 'delivery_id' => $deliveryId, 'error' => $e->getMessage(), ]); return false; } } /** * Update delivery tracking with rider location */ public function updateDeliveryTracking( int|string $deliveryId, array $updates ): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $path = str_replace('{delivery_id}', (string) $deliveryId, $this->config['paths']['delivery_tracking']); $updates['updated_at'] = now()->toIso8601String(); $database->getReference($path)->update($updates); return true; } catch (FirebaseException $e) { Log::error('Failed to update delivery tracking in Firebase', [ 'delivery_id' => $deliveryId, 'error' => $e->getMessage(), ]); return false; } } /** * Update delivery status in Firebase */ public function updateDeliveryStatus( int|string $deliveryId, string $status, ?array $metadata = null ): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $statusPath = str_replace('{delivery_id}', (string) $deliveryId, $this->config['paths']['delivery_status']); $trackingPath = str_replace('{delivery_id}', (string) $deliveryId, $this->config['paths']['delivery_tracking']); $statusConfig = config("restaurant-delivery.delivery_flow.statuses.{$status}"); $statusData = [ 'status' => $status, 'label' => $statusConfig['label'] ?? $status, 'description' => $statusConfig['description'] ?? null, 'color' => $statusConfig['color'] ?? '#6B7280', 'timestamp' => time() * 1000, 'updated_at' => now()->toIso8601String(), ]; if ($metadata) { $statusData = array_merge($statusData, $metadata); } // Update both status and tracking paths $database->getReference($statusPath)->set($statusData); $database->getReference($trackingPath.'/status')->set($status); $database->getReference($trackingPath.'/status_data')->set($statusData); return true; } catch (FirebaseException $e) { Log::error('Failed to update delivery status in Firebase', [ 'delivery_id' => $deliveryId, 'status' => $status, 'error' => $e->getMessage(), ]); return false; } } /** * Get delivery tracking data */ public function getDeliveryTracking(int|string $deliveryId): ?array { try { $database = $this->getDatabase(); if (! $database) { return null; } $path = str_replace('{delivery_id}', (string) $deliveryId, $this->config['paths']['delivery_tracking']); $snapshot = $database->getReference($path)->getSnapshot(); return $snapshot->exists() ? $snapshot->getValue() : null; } catch (FirebaseException $e) { Log::error('Failed to get delivery tracking from Firebase', [ 'delivery_id' => $deliveryId, 'error' => $e->getMessage(), ]); return null; } } /** * Update rider location for a specific delivery */ public function updateDeliveryRiderLocation( int|string $deliveryId, float $latitude, float $longitude, ?float $speed = null, ?float $bearing = null, ?float $eta = null, ?float $remainingDistance = null ): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $path = str_replace('{delivery_id}', (string) $deliveryId, $this->config['paths']['delivery_tracking']); $locationData = [ 'rider_location' => [ 'lat' => $latitude, 'lng' => $longitude, 'speed' => $speed ?? 0, 'bearing' => $bearing ?? 0, 'timestamp' => time() * 1000, ], 'eta' => $eta, 'remaining_distance' => $remainingDistance, 'updated_at' => now()->toIso8601String(), ]; $database->getReference($path)->update($locationData); return true; } catch (FirebaseException $e) { Log::error('Failed to update delivery rider location in Firebase', [ 'delivery_id' => $deliveryId, 'error' => $e->getMessage(), ]); return false; } } /** * Store route polyline for delivery */ public function updateDeliveryRoute( int|string $deliveryId, array $route ): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $path = str_replace('{delivery_id}', (string) $deliveryId, $this->config['paths']['delivery_tracking']); $database->getReference($path.'/route')->set($route); return true; } catch (FirebaseException $e) { Log::error('Failed to update delivery route in Firebase', [ 'delivery_id' => $deliveryId, 'error' => $e->getMessage(), ]); return false; } } /** * Remove delivery tracking data (cleanup after delivery) */ public function removeDeliveryTracking(int|string $deliveryId): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $trackingPath = str_replace('{delivery_id}', (string) $deliveryId, $this->config['paths']['delivery_tracking']); $statusPath = str_replace('{delivery_id}', (string) $deliveryId, $this->config['paths']['delivery_status']); $database->getReference($trackingPath)->remove(); $database->getReference($statusPath)->remove(); return true; } catch (FirebaseException $e) { Log::error('Failed to remove delivery tracking from Firebase', [ 'delivery_id' => $deliveryId, 'error' => $e->getMessage(), ]); return false; } } /* |-------------------------------------------------------------------------- | Rider Assignment Operations |-------------------------------------------------------------------------- */ /** * Add delivery to rider's assignment list */ public function addRiderAssignment( int|string $riderId, int|string $deliveryId, array $deliveryInfo ): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $path = str_replace('{rider_id}', (string) $riderId, $this->config['paths']['rider_assignments']); $assignmentData = [ 'delivery_id' => $deliveryId, 'status' => 'assigned', 'restaurant' => $deliveryInfo['restaurant'] ?? null, 'customer_address' => $deliveryInfo['customer_address'] ?? null, 'pickup_location' => $deliveryInfo['pickup_location'] ?? null, 'drop_location' => $deliveryInfo['drop_location'] ?? null, 'assigned_at' => now()->toIso8601String(), 'timestamp' => time() * 1000, ]; $database->getReference($path.'/'.$deliveryId)->set($assignmentData); return true; } catch (FirebaseException $e) { Log::error('Failed to add rider assignment in Firebase', [ 'rider_id' => $riderId, 'delivery_id' => $deliveryId, 'error' => $e->getMessage(), ]); return false; } } /** * Remove delivery from rider's assignment list */ public function removeRiderAssignment( int|string $riderId, int|string $deliveryId ): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $path = str_replace('{rider_id}', (string) $riderId, $this->config['paths']['rider_assignments']); $database->getReference($path.'/'.$deliveryId)->remove(); return true; } catch (FirebaseException $e) { Log::error('Failed to remove rider assignment from Firebase', [ 'rider_id' => $riderId, 'delivery_id' => $deliveryId, 'error' => $e->getMessage(), ]); return false; } } /** * Get all active assignments for a rider */ public function getRiderAssignments(int|string $riderId): array { try { $database = $this->getDatabase(); if (! $database) { return []; } $path = str_replace('{rider_id}', (string) $riderId, $this->config['paths']['rider_assignments']); $snapshot = $database->getReference($path)->getSnapshot(); return $snapshot->exists() ? $snapshot->getValue() : []; } catch (FirebaseException $e) { Log::error('Failed to get rider assignments from Firebase', [ 'rider_id' => $riderId, 'error' => $e->getMessage(), ]); return []; } } /* |-------------------------------------------------------------------------- | Push Notifications |-------------------------------------------------------------------------- */ /** * Send push notification to a device */ public function sendPushNotification( string $token, string $title, string $body, ?array $data = null ): bool { try { $messaging = $this->getMessaging(); if (! $messaging) { return false; } $message = CloudMessage::withTarget('token', $token) ->withNotification(Notification::create($title, $body)); if ($data) { $message = $message->withData($data); } $messaging->send($message); return true; } catch (FirebaseException $e) { Log::error('Failed to send push notification', [ 'error' => $e->getMessage(), ]); return false; } } /** * Send push notification to multiple devices */ public function sendMulticastNotification( array $tokens, string $title, string $body, ?array $data = null ): array { try { $messaging = $this->getMessaging(); if (! $messaging) { return ['success' => 0, 'failure' => count($tokens)]; } $message = CloudMessage::new() ->withNotification(Notification::create($title, $body)); if ($data) { $message = $message->withData($data); } $report = $messaging->sendMulticast($message, $tokens); return [ 'success' => $report->successes()->count(), 'failure' => $report->failures()->count(), 'invalid_tokens' => $report->invalidTokens(), ]; } catch (FirebaseException $e) { Log::error('Failed to send multicast notification', [ 'error' => $e->getMessage(), ]); return ['success' => 0, 'failure' => count($tokens)]; } } /** * Send notification to a topic */ public function sendTopicNotification( string $topic, string $title, string $body, ?array $data = null ): bool { try { $messaging = $this->getMessaging(); if (! $messaging) { return false; } $message = CloudMessage::withTarget('topic', $topic) ->withNotification(Notification::create($title, $body)); if ($data) { $message = $message->withData($data); } $messaging->send($message); return true; } catch (FirebaseException $e) { Log::error('Failed to send topic notification', [ 'topic' => $topic, 'error' => $e->getMessage(), ]); return false; } } /** * Subscribe device to a topic */ public function subscribeToTopic(string $token, string $topic): bool { try { $messaging = $this->getMessaging(); if (! $messaging) { return false; } $messaging->subscribeToTopic($topic, [$token]); return true; } catch (FirebaseException $e) { Log::error('Failed to subscribe to topic', [ 'topic' => $topic, 'error' => $e->getMessage(), ]); return false; } } /** * Unsubscribe device from a topic */ public function unsubscribeFromTopic(string $token, string $topic): bool { try { $messaging = $this->getMessaging(); if (! $messaging) { return false; } $messaging->unsubscribeFromTopic($topic, [$token]); return true; } catch (FirebaseException $e) { Log::error('Failed to unsubscribe from topic', [ 'topic' => $topic, 'error' => $e->getMessage(), ]); return false; } } /* |-------------------------------------------------------------------------- | Utility Methods |-------------------------------------------------------------------------- */ /** * Set a value at a custom path */ public function set(string $path, mixed $value): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $database->getReference($path)->set($value); return true; } catch (FirebaseException $e) { Log::error('Failed to set value in Firebase', [ 'path' => $path, 'error' => $e->getMessage(), ]); return false; } } /** * Update values at a custom path */ public function update(string $path, array $values): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $database->getReference($path)->update($values); return true; } catch (FirebaseException $e) { Log::error('Failed to update value in Firebase', [ 'path' => $path, 'error' => $e->getMessage(), ]); return false; } } /** * Get value from a custom path */ public function get(string $path): mixed { try { $database = $this->getDatabase(); if (! $database) { return null; } $snapshot = $database->getReference($path)->getSnapshot(); return $snapshot->exists() ? $snapshot->getValue() : null; } catch (FirebaseException $e) { Log::error('Failed to get value from Firebase', [ 'path' => $path, 'error' => $e->getMessage(), ]); return null; } } /** * Delete value at a custom path */ public function delete(string $path): bool { try { $database = $this->getDatabase(); if (! $database) { return false; } $database->getReference($path)->remove(); return true; } catch (FirebaseException $e) { Log::error('Failed to delete value from Firebase', [ 'path' => $path, 'error' => $e->getMessage(), ]); return false; } } }