migrate to gtea from bistbucket
This commit is contained in:
@@ -0,0 +1,597 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\RestaurantDelivery\Services\Firebase;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Kreait\Firebase\Auth;
|
||||
use Kreait\Firebase\Auth\CreateRequest;
|
||||
use Kreait\Firebase\Exception\Auth\FailedToVerifyToken;
|
||||
use Kreait\Firebase\Exception\FirebaseException;
|
||||
use Kreait\Firebase\Factory;
|
||||
|
||||
/**
|
||||
* Firebase Authentication Service
|
||||
*
|
||||
* Handles user authentication, custom tokens, and claims for role-based access.
|
||||
*/
|
||||
class FirebaseAuthService
|
||||
{
|
||||
protected ?Auth $auth = null;
|
||||
|
||||
protected Factory $factory;
|
||||
|
||||
protected array $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('restaurant-delivery.firebase');
|
||||
$this->initializeFirebase();
|
||||
}
|
||||
|
||||
protected function initializeFirebase(): void
|
||||
{
|
||||
if (! $this->config['enabled']) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->factory = (new Factory)
|
||||
->withServiceAccount($this->config['credentials_path']);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Firebase Auth initialization failed', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getAuth(): ?Auth
|
||||
{
|
||||
if (! $this->auth && isset($this->factory)) {
|
||||
$this->auth = $this->factory->createAuth();
|
||||
}
|
||||
|
||||
return $this->auth;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Token Generation
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a custom token for a user with custom claims
|
||||
*
|
||||
* @param string $uid User ID
|
||||
* @param array $claims Custom claims (role, permissions, etc.)
|
||||
* @return string|null JWT token
|
||||
*/
|
||||
public function createCustomToken(string $uid, array $claims = []): ?string
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$customToken = $auth->createCustomToken($uid, $claims);
|
||||
|
||||
return $customToken->toString();
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to create custom token', [
|
||||
'uid' => $uid,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom token for a rider with appropriate claims
|
||||
*/
|
||||
public function createRiderToken(string $riderId, array $additionalClaims = []): ?string
|
||||
{
|
||||
$claims = array_merge([
|
||||
'role' => 'rider',
|
||||
'rider_id' => $riderId,
|
||||
'can_update_location' => true,
|
||||
'can_accept_deliveries' => true,
|
||||
], $additionalClaims);
|
||||
|
||||
return $this->createCustomToken($riderId, $claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom token for a restaurant with appropriate claims
|
||||
*/
|
||||
public function createRestaurantToken(string $restaurantId, array $additionalClaims = []): ?string
|
||||
{
|
||||
$claims = array_merge([
|
||||
'role' => 'restaurant',
|
||||
'restaurant_id' => $restaurantId,
|
||||
'restaurant' => true,
|
||||
'can_create_deliveries' => true,
|
||||
'can_assign_riders' => true,
|
||||
], $additionalClaims);
|
||||
|
||||
return $this->createCustomToken($restaurantId, $claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom token for a customer with appropriate claims
|
||||
*/
|
||||
public function createCustomerToken(string $customerId, array $additionalClaims = []): ?string
|
||||
{
|
||||
$claims = array_merge([
|
||||
'role' => 'customer',
|
||||
'customer_id' => $customerId,
|
||||
'can_track_deliveries' => true,
|
||||
'can_rate_riders' => true,
|
||||
], $additionalClaims);
|
||||
|
||||
return $this->createCustomToken($customerId, $claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom token for an admin with full permissions
|
||||
*/
|
||||
public function createAdminToken(string $adminId, array $additionalClaims = []): ?string
|
||||
{
|
||||
$claims = array_merge([
|
||||
'role' => 'admin',
|
||||
'admin' => true,
|
||||
'admin_id' => $adminId,
|
||||
'full_access' => true,
|
||||
], $additionalClaims);
|
||||
|
||||
return $this->createCustomToken($adminId, $claims);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Token Verification
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Verify an ID token and return the decoded claims
|
||||
*/
|
||||
public function verifyIdToken(string $idToken): ?array
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$verifiedToken = $auth->verifyIdToken($idToken);
|
||||
|
||||
return [
|
||||
'uid' => $verifiedToken->claims()->get('sub'),
|
||||
'email' => $verifiedToken->claims()->get('email'),
|
||||
'email_verified' => $verifiedToken->claims()->get('email_verified'),
|
||||
'claims' => $verifiedToken->claims()->all(),
|
||||
];
|
||||
} catch (FailedToVerifyToken $e) {
|
||||
Log::warning('Failed to verify ID token', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Firebase error during token verification', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token has specific role
|
||||
*/
|
||||
public function hasRole(string $idToken, string $role): bool
|
||||
{
|
||||
$decoded = $this->verifyIdToken($idToken);
|
||||
|
||||
if (! $decoded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($decoded['claims']['role'] ?? null) === $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token has admin access
|
||||
*/
|
||||
public function isAdmin(string $idToken): bool
|
||||
{
|
||||
$decoded = $this->verifyIdToken($idToken);
|
||||
|
||||
if (! $decoded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($decoded['claims']['admin'] ?? false) === true;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Claims Management
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set custom claims for a user
|
||||
*/
|
||||
public function setCustomClaims(string $uid, array $claims): bool
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$auth->setCustomUserClaims($uid, $claims);
|
||||
|
||||
// Invalidate cached claims
|
||||
Cache::forget("firebase_claims_{$uid}");
|
||||
|
||||
return true;
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to set custom claims', [
|
||||
'uid' => $uid,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's custom claims
|
||||
*/
|
||||
public function getCustomClaims(string $uid): array
|
||||
{
|
||||
// Try cache first
|
||||
$cached = Cache::get("firebase_claims_{$uid}");
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$user = $auth->getUser($uid);
|
||||
$claims = $user->customClaims ?? [];
|
||||
|
||||
// Cache for 5 minutes
|
||||
Cache::put("firebase_claims_{$uid}", $claims, 300);
|
||||
|
||||
return $claims;
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to get custom claims', [
|
||||
'uid' => $uid,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add role to user
|
||||
*/
|
||||
public function addRole(string $uid, string $role): bool
|
||||
{
|
||||
$claims = $this->getCustomClaims($uid);
|
||||
$claims['role'] = $role;
|
||||
|
||||
// Add role-specific flags
|
||||
switch ($role) {
|
||||
case 'admin':
|
||||
$claims['admin'] = true;
|
||||
break;
|
||||
case 'restaurant':
|
||||
$claims['restaurant'] = true;
|
||||
break;
|
||||
case 'rider':
|
||||
$claims['rider'] = true;
|
||||
break;
|
||||
case 'customer':
|
||||
$claims['customer'] = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->setCustomClaims($uid, $claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove role from user
|
||||
*/
|
||||
public function removeRole(string $uid, string $role): bool
|
||||
{
|
||||
$claims = $this->getCustomClaims($uid);
|
||||
|
||||
if (isset($claims['role']) && $claims['role'] === $role) {
|
||||
unset($claims['role']);
|
||||
}
|
||||
|
||||
// Remove role-specific flags
|
||||
unset($claims[$role]);
|
||||
|
||||
return $this->setCustomClaims($uid, $claims);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Management
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a new Firebase user
|
||||
*/
|
||||
public function createUser(array $properties): ?array
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$request = CreateRequest::new();
|
||||
|
||||
if (isset($properties['email'])) {
|
||||
$request = $request->withEmail($properties['email']);
|
||||
}
|
||||
if (isset($properties['password'])) {
|
||||
$request = $request->withPassword($properties['password']);
|
||||
}
|
||||
if (isset($properties['phone'])) {
|
||||
$request = $request->withPhoneNumber($properties['phone']);
|
||||
}
|
||||
if (isset($properties['display_name'])) {
|
||||
$request = $request->withDisplayName($properties['display_name']);
|
||||
}
|
||||
if (isset($properties['photo_url'])) {
|
||||
$request = $request->withPhotoUrl($properties['photo_url']);
|
||||
}
|
||||
if (isset($properties['disabled'])) {
|
||||
$request = $properties['disabled'] ? $request->markAsDisabled() : $request->markAsEnabled();
|
||||
}
|
||||
if (isset($properties['email_verified'])) {
|
||||
$request = $properties['email_verified'] ? $request->markEmailAsVerified() : $request->markEmailAsUnverified();
|
||||
}
|
||||
if (isset($properties['uid'])) {
|
||||
$request = $request->withUid($properties['uid']);
|
||||
}
|
||||
|
||||
$user = $auth->createUser($request);
|
||||
|
||||
return [
|
||||
'uid' => $user->uid,
|
||||
'email' => $user->email,
|
||||
'phone' => $user->phoneNumber,
|
||||
'display_name' => $user->displayName,
|
||||
'photo_url' => $user->photoUrl,
|
||||
'disabled' => $user->disabled,
|
||||
'email_verified' => $user->emailVerified,
|
||||
];
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to create Firebase user', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by UID
|
||||
*/
|
||||
public function getUser(string $uid): ?array
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $auth->getUser($uid);
|
||||
|
||||
return [
|
||||
'uid' => $user->uid,
|
||||
'email' => $user->email,
|
||||
'phone' => $user->phoneNumber,
|
||||
'display_name' => $user->displayName,
|
||||
'photo_url' => $user->photoUrl,
|
||||
'disabled' => $user->disabled,
|
||||
'email_verified' => $user->emailVerified,
|
||||
'custom_claims' => $user->customClaims,
|
||||
'created_at' => $user->metadata->createdAt,
|
||||
'last_login' => $user->metadata->lastLoginAt,
|
||||
];
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to get Firebase user', [
|
||||
'uid' => $uid,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by email
|
||||
*/
|
||||
public function getUserByEmail(string $email): ?array
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $auth->getUserByEmail($email);
|
||||
|
||||
return [
|
||||
'uid' => $user->uid,
|
||||
'email' => $user->email,
|
||||
'phone' => $user->phoneNumber,
|
||||
'display_name' => $user->displayName,
|
||||
'disabled' => $user->disabled,
|
||||
];
|
||||
} catch (FirebaseException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user properties
|
||||
*/
|
||||
public function updateUser(string $uid, array $properties): bool
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$auth->updateUser($uid, $properties);
|
||||
|
||||
return true;
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to update Firebase user', [
|
||||
'uid' => $uid,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a user
|
||||
*/
|
||||
public function disableUser(string $uid): bool
|
||||
{
|
||||
return $this->updateUser($uid, ['disabled' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a user
|
||||
*/
|
||||
public function enableUser(string $uid): bool
|
||||
{
|
||||
return $this->updateUser($uid, ['disabled' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user
|
||||
*/
|
||||
public function deleteUser(string $uid): bool
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$auth->deleteUser($uid);
|
||||
|
||||
// Clear cached claims
|
||||
Cache::forget("firebase_claims_{$uid}");
|
||||
|
||||
return true;
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to delete Firebase user', [
|
||||
'uid' => $uid,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Management
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generate password reset link
|
||||
*/
|
||||
public function generatePasswordResetLink(string $email): ?string
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $auth->getPasswordResetLink($email);
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to generate password reset link', [
|
||||
'email' => $email,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate email verification link
|
||||
*/
|
||||
public function generateEmailVerificationLink(string $email): ?string
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $auth->getEmailVerificationLink($email);
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to generate email verification link', [
|
||||
'email' => $email,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke all refresh tokens for a user
|
||||
*/
|
||||
public function revokeRefreshTokens(string $uid): bool
|
||||
{
|
||||
try {
|
||||
$auth = $this->getAuth();
|
||||
if (! $auth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$auth->revokeRefreshTokens($uid);
|
||||
|
||||
return true;
|
||||
} catch (FirebaseException $e) {
|
||||
Log::error('Failed to revoke refresh tokens', [
|
||||
'uid' => $uid,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,885 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\RestaurantDelivery\Services\Firebase;
|
||||
|
||||
use Google\Cloud\Firestore\FirestoreClient;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FirestoreService
|
||||
{
|
||||
protected ?FirestoreClient $firestore = null;
|
||||
|
||||
protected array $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = config('restaurant-delivery.firebase');
|
||||
$this->initializeFirestore();
|
||||
}
|
||||
|
||||
protected function initializeFirestore(): void
|
||||
{
|
||||
if (! $this->config['firestore']['enabled']) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->firestore = new FirestoreClient([
|
||||
'keyFilePath' => $this->config['credentials_path'],
|
||||
'projectId' => $this->config['project_id'],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Firestore initialization failed', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getFirestore(): ?FirestoreClient
|
||||
{
|
||||
return $this->firestore;
|
||||
}
|
||||
|
||||
protected function getCollectionName(string $type): string
|
||||
{
|
||||
return $this->config['firestore']['collections'][$type] ?? $type;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delivery Operations
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create delivery document in Firestore
|
||||
*/
|
||||
public function createDelivery(int|string $deliveryId, array $data): bool
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('deliveries');
|
||||
$docRef = $this->firestore->collection($collection)->document((string) $deliveryId);
|
||||
|
||||
$deliveryData = array_merge($data, [
|
||||
'created_at' => new \Google\Cloud\Core\Timestamp(new \DateTime),
|
||||
'updated_at' => new \Google\Cloud\Core\Timestamp(new \DateTime),
|
||||
]);
|
||||
|
||||
$docRef->set($deliveryData);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to create delivery in Firestore', [
|
||||
'delivery_id' => $deliveryId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update delivery document
|
||||
*/
|
||||
public function updateDelivery(int|string $deliveryId, array $data): bool
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('deliveries');
|
||||
$docRef = $this->firestore->collection($collection)->document((string) $deliveryId);
|
||||
|
||||
$data['updated_at'] = new \Google\Cloud\Core\Timestamp(new \DateTime);
|
||||
|
||||
$docRef->update($this->formatForUpdate($data));
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to update delivery in Firestore', [
|
||||
'delivery_id' => $deliveryId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get delivery document
|
||||
*/
|
||||
public function getDelivery(int|string $deliveryId): ?array
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('deliveries');
|
||||
$docRef = $this->firestore->collection($collection)->document((string) $deliveryId);
|
||||
$snapshot = $docRef->snapshot();
|
||||
|
||||
return $snapshot->exists() ? $snapshot->data() : null;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to get delivery from Firestore', [
|
||||
'delivery_id' => $deliveryId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete delivery document
|
||||
*/
|
||||
public function deleteDelivery(int|string $deliveryId): bool
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('deliveries');
|
||||
$this->firestore->collection($collection)->document((string) $deliveryId)->delete();
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to delete delivery from Firestore', [
|
||||
'delivery_id' => $deliveryId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query active deliveries for a rider
|
||||
*/
|
||||
public function getRiderActiveDeliveries(int|string $riderId): array
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('deliveries');
|
||||
$query = $this->firestore->collection($collection)
|
||||
->where('rider_id', '=', $riderId)
|
||||
->where('status', 'in', ['assigned', 'picked_up', 'on_the_way']);
|
||||
|
||||
$deliveries = [];
|
||||
foreach ($query->documents() as $document) {
|
||||
if ($document->exists()) {
|
||||
$deliveries[] = array_merge(['id' => $document->id()], $document->data());
|
||||
}
|
||||
}
|
||||
|
||||
return $deliveries;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to get rider active deliveries from Firestore', [
|
||||
'rider_id' => $riderId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query deliveries for a restaurant
|
||||
*/
|
||||
public function getRestaurantDeliveries(
|
||||
int|string $restaurantId,
|
||||
?string $status = null,
|
||||
int $limit = 50
|
||||
): array {
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('deliveries');
|
||||
$query = $this->firestore->collection($collection)
|
||||
->where('restaurant_id', '=', $restaurantId)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit);
|
||||
|
||||
if ($status) {
|
||||
$query = $query->where('status', '=', $status);
|
||||
}
|
||||
|
||||
$deliveries = [];
|
||||
foreach ($query->documents() as $document) {
|
||||
if ($document->exists()) {
|
||||
$deliveries[] = array_merge(['id' => $document->id()], $document->data());
|
||||
}
|
||||
}
|
||||
|
||||
return $deliveries;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to get restaurant deliveries from Firestore', [
|
||||
'restaurant_id' => $restaurantId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Rider Operations
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update rider document
|
||||
*/
|
||||
public function updateRider(int|string $riderId, array $data): bool
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('riders');
|
||||
$docRef = $this->firestore->collection($collection)->document((string) $riderId);
|
||||
|
||||
$data['updated_at'] = new \Google\Cloud\Core\Timestamp(new \DateTime);
|
||||
|
||||
$docRef->set($data, ['merge' => true]);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to update rider in Firestore', [
|
||||
'rider_id' => $riderId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rider document
|
||||
*/
|
||||
public function getRider(int|string $riderId): ?array
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('riders');
|
||||
$snapshot = $this->firestore->collection($collection)
|
||||
->document((string) $riderId)
|
||||
->snapshot();
|
||||
|
||||
return $snapshot->exists() ? $snapshot->data() : null;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to get rider from Firestore', [
|
||||
'rider_id' => $riderId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query online riders within a radius
|
||||
*/
|
||||
public function getOnlineRidersInArea(
|
||||
float $latitude,
|
||||
float $longitude,
|
||||
float $radiusKm
|
||||
): array {
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Note: Firestore doesn't support geo queries natively
|
||||
// You would typically use Geohashing or Firebase GeoFire for this
|
||||
// This is a simplified version that queries all online riders
|
||||
// and filters by distance in PHP
|
||||
|
||||
$collection = $this->getCollectionName('riders');
|
||||
$query = $this->firestore->collection($collection)
|
||||
->where('is_online', '=', true)
|
||||
->where('status', '=', 'available');
|
||||
|
||||
$riders = [];
|
||||
foreach ($query->documents() as $document) {
|
||||
if ($document->exists()) {
|
||||
$data = $document->data();
|
||||
if (isset($data['location'])) {
|
||||
$distance = $this->calculateDistance(
|
||||
$latitude,
|
||||
$longitude,
|
||||
$data['location']['lat'],
|
||||
$data['location']['lng']
|
||||
);
|
||||
|
||||
if ($distance <= $radiusKm) {
|
||||
$riders[] = array_merge(
|
||||
['id' => $document->id(), 'distance' => $distance],
|
||||
$data
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by distance
|
||||
usort($riders, fn ($a, $b) => $a['distance'] <=> $b['distance']);
|
||||
|
||||
return $riders;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to get online riders from Firestore', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Tracking History Operations
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add tracking point to history
|
||||
*/
|
||||
public function addTrackingPoint(
|
||||
int|string $deliveryId,
|
||||
float $latitude,
|
||||
float $longitude,
|
||||
array $metadata = []
|
||||
): bool {
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('tracking_history');
|
||||
$docRef = $this->firestore->collection($collection)
|
||||
->document((string) $deliveryId)
|
||||
->collection('points')
|
||||
->newDocument();
|
||||
|
||||
$pointData = array_merge([
|
||||
'lat' => $latitude,
|
||||
'lng' => $longitude,
|
||||
'timestamp' => new \Google\Cloud\Core\Timestamp(new \DateTime),
|
||||
], $metadata);
|
||||
|
||||
$docRef->set($pointData);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to add tracking point to Firestore', [
|
||||
'delivery_id' => $deliveryId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracking history for a delivery
|
||||
*/
|
||||
public function getTrackingHistory(int|string $deliveryId, int $limit = 100): array
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('tracking_history');
|
||||
$query = $this->firestore->collection($collection)
|
||||
->document((string) $deliveryId)
|
||||
->collection('points')
|
||||
->orderBy('timestamp', 'desc')
|
||||
->limit($limit);
|
||||
|
||||
$points = [];
|
||||
foreach ($query->documents() as $document) {
|
||||
if ($document->exists()) {
|
||||
$points[] = $document->data();
|
||||
}
|
||||
}
|
||||
|
||||
return array_reverse($points); // Return in chronological order
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to get tracking history from Firestore', [
|
||||
'delivery_id' => $deliveryId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Rating Operations
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create rating document
|
||||
*/
|
||||
public function createRating(array $ratingData): ?string
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('ratings');
|
||||
$docRef = $this->firestore->collection($collection)->newDocument();
|
||||
|
||||
$ratingData['created_at'] = new \Google\Cloud\Core\Timestamp(new \DateTime);
|
||||
|
||||
$docRef->set($ratingData);
|
||||
|
||||
return $docRef->id();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to create rating in Firestore', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ratings for a rider
|
||||
*/
|
||||
public function getRiderRatings(int|string $riderId, int $limit = 50): array
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('ratings');
|
||||
$query = $this->firestore->collection($collection)
|
||||
->where('rider_id', '=', $riderId)
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit);
|
||||
|
||||
$ratings = [];
|
||||
foreach ($query->documents() as $document) {
|
||||
if ($document->exists()) {
|
||||
$ratings[] = array_merge(['id' => $document->id()], $document->data());
|
||||
}
|
||||
}
|
||||
|
||||
return $ratings;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to get rider ratings from Firestore', [
|
||||
'rider_id' => $riderId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate rider average rating
|
||||
*/
|
||||
public function calculateRiderAverageRating(int|string $riderId): ?float
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$collection = $this->getCollectionName('ratings');
|
||||
$query = $this->firestore->collection($collection)
|
||||
->where('rider_id', '=', $riderId);
|
||||
|
||||
$totalRating = 0;
|
||||
$count = 0;
|
||||
|
||||
foreach ($query->documents() as $document) {
|
||||
if ($document->exists()) {
|
||||
$data = $document->data();
|
||||
if (isset($data['overall_rating'])) {
|
||||
$totalRating += $data['overall_rating'];
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $count > 0 ? round($totalRating / $count, 2) : null;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to calculate rider average rating from Firestore', [
|
||||
'rider_id' => $riderId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Utility Methods
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format data for Firestore update operation
|
||||
*/
|
||||
protected function formatForUpdate(array $data): array
|
||||
{
|
||||
$formatted = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$formatted[] = ['path' => $key, 'value' => $value];
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points using Haversine formula
|
||||
*/
|
||||
protected function calculateDistance(
|
||||
float $lat1,
|
||||
float $lng1,
|
||||
float $lat2,
|
||||
float $lng2
|
||||
): float {
|
||||
$earthRadius = 6371; // km
|
||||
|
||||
$dLat = deg2rad($lat2 - $lat1);
|
||||
$dLng = deg2rad($lng2 - $lng1);
|
||||
|
||||
$a = sin($dLat / 2) * sin($dLat / 2) +
|
||||
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
||||
sin($dLng / 2) * sin($dLng / 2);
|
||||
|
||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
||||
|
||||
return $earthRadius * $c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a transaction
|
||||
*/
|
||||
public function runTransaction(callable $callback): mixed
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->firestore->runTransaction($callback);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Firestore transaction failed', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch write multiple documents
|
||||
*/
|
||||
public function batchWrite(array $operations): bool
|
||||
{
|
||||
try {
|
||||
if (! $this->firestore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$batch = $this->firestore->batch();
|
||||
|
||||
foreach ($operations as $op) {
|
||||
$docRef = $this->firestore
|
||||
->collection($op['collection'])
|
||||
->document($op['document']);
|
||||
|
||||
switch ($op['type']) {
|
||||
case 'set':
|
||||
$batch->set($docRef, $op['data']);
|
||||
break;
|
||||
case 'update':
|
||||
$batch->update($docRef, $this->formatForUpdate($op['data']));
|
||||
break;
|
||||
case 'delete':
|
||||
$batch->delete($docRef);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$batch->commit();
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Firestore batch write failed', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user