migrate to gtea from bistbucket

This commit is contained in:
2026-03-15 17:08:23 +07:00
commit 129ca2260c
3716 changed files with 566316 additions and 0 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}