Files

886 lines
26 KiB
PHP

<?php
declare(strict_types=1);
namespace Modules\RestaurantDelivery\Services\Firebase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Kreait\Firebase\Database;
use Kreait\Firebase\Exception\FirebaseException;
use Kreait\Firebase\Factory;
use Kreait\Firebase\Messaging;
use Kreait\Firebase\Messaging\CloudMessage;
use Kreait\Firebase\Messaging\Notification;
class FirebaseService
{
protected ?Database $database = null;
protected ?Messaging $messaging = null;
protected ?Factory $factory = null; // nullable
protected array $config;
public function __construct()
{
$this->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;
}
}
}