# WebSocket Broadcasting Implementation Guide - Backend (Laravel)

## 📋 Overview

This document provides a comprehensive guide on how WebSocket broadcasting is implemented in the Laravel backend using Laravel Reverb. It explains the architecture, components, and step-by-step implementation details that can be used as a reference for other projects.

**Technology Stack:**

-   **Laravel Reverb**: Official Laravel WebSocket solution
-   **Laravel Broadcasting**: Built-in broadcasting system
-   **Model Events**: Eloquent model lifecycle hooks
-   **Channel Authorization**: Custom authorization logic

---

## 🏗️ Architecture Overview

### How It Works

```
┌─────────────────┐
│  Laravel Model  │
│   (Schedule)    │
└────────┬────────┘
         │ Model Events (created/updated/deleted)
         ▼
┌─────────────────┐
│  ScheduleEvent  │
│   (Broadcast)    │
└────────┬────────┘
         │ broadcast()
         ▼
┌─────────────────┐
│ Laravel         │
│ Broadcasting    │
│ System          │
└────────┬────────┘
         │ Redis Pub/Sub
         ▼
┌─────────────────┐
│  Reverb Server   │
│   (WebSocket)    │
└────────┬────────┘
         │ WebSocket Protocol
         ▼
┌─────────────────┐
│  Frontend Client│
│  (Next.js/React)│
└─────────────────┘
```

**Note:** Laravel Broadcasting uses Redis pub/sub internally to communicate between the Laravel application and the Reverb server. This allows for horizontal scaling and decoupling of the broadcasting system.

### Key Components

1. **Event Class** (`ScheduleEvent`): Defines what data to broadcast and which channel
2. **Model Events** (`Schedule::booted()`): Triggers broadcasting when data changes
3. **Channel Authorization** (`routes/channels.php`): Controls who can subscribe
4. **Broadcasting Configuration** (`config/broadcasting.php`): Configures Reverb connection
5. **Helper Classes** (`BroadcastingHelper`): Reusable authorization logic

---

## 📦 Prerequisites & Installation

### Step 1: Install Required Packages

**Install Laravel Reverb:**

```bash
cd /path/to/your/laravel/project
composer require laravel/reverb
```

**Publish Reverb Configuration:**

```bash
php artisan reverb:install
```

This command will:

-   Create `config/reverb.php` configuration file
-   Generate Reverb application keys (APP_ID, APP_KEY, APP_SECRET)
-   Update `config/broadcasting.php` if needed

**Note:** Copy the generated keys to your `.env` file (see configuration section).

### Step 2: Verify Redis Installation

Laravel Broadcasting requires Redis for pub/sub communication.

**Check if Redis is installed:**

```bash
redis-cli ping
# Should return: PONG
```

**If Redis is not installed (Ubuntu/Debian):**

```bash
sudo apt-get update
sudo apt-get install redis-server -y
sudo systemctl start redis-server
sudo systemctl enable redis-server
```

**Verify Redis connection in Laravel:**

```bash
php artisan tinker
>>> Redis::ping()
# Should return: "PONG"
```

**Configure Redis in `.env`:**

```env
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null  # or your Redis password
REDIS_PORT=6379
REDIS_DB=0
```

---

## 📝 Step-by-Step Implementation

### Step 1: Create the Broadcast Event Class

**File:** `app/Events/ScheduleEvent.php`

This event class implements `ShouldBroadcastNow` to broadcast immediately (synchronously) without queuing.

```php
<?php

namespace App\Events;

use App\Models\Schedule;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ScheduleEvent implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $action;
    public $locationId;
    public $schedule;
    public $scheduleId;

    /**
     * Create a new event instance.
     *
     * @param string $action Action type: 'created', 'updated', 'deleted', 'order_changed'
     * @param int $locationId Location ID for channel routing
     * @param int|null $scheduleId Schedule ID (for created/updated/deleted actions)
     */
    public function __construct(
        string $action,
        int $locationId,
        ?int $scheduleId = null
    ) {
        $this->action = $action;
        $this->locationId = $locationId;
        $this->scheduleId = $scheduleId;

        // Format schedule with same structure as index method for created/updated actions
        if (in_array($action, ['created', 'updated']) && $scheduleId) {
            $this->schedule = $this->formatScheduleForTable($scheduleId);
        }
    }
}
```

**Key Points:**

-   `ShouldBroadcastNow`: Broadcasts immediately (no queue)
-   Constructor formats data before broadcasting
-   Action types: `created`, `updated`, `deleted`, `order_changed`

### Step 2: Define Channel and Broadcast Name

**In the same `ScheduleEvent` class:**

```php
/**
 * Get the channels the event should broadcast on.
 */
public function broadcastOn(): array
{
    return [
        new Channel('schedule.location.' . $this->locationId),
    ];
}

/**
 * The event's broadcast name.
 */
public function broadcastAs(): string
{
    return 'schedule.event';
}

/**
 * Get the data to broadcast.
 */
public function broadcastWith(): array
{
    $data = [
        'action' => $this->action,
        'location_id' => $this->locationId,
    ];

    // Include schedule data for created/updated actions
    if (in_array($this->action, ['created', 'updated']) && $this->schedule) {
        try {
            $scheduleArray = $this->schedule->makeHidden(['container_location'])->toArray();
            $data['schedule'] = $scheduleArray;
        } catch (\Exception $e) {
            $data['schedule'] = null;
            $data['schedule_error'] = 'Failed to serialize schedule data';
        }
    }

    // Include schedule ID for deleted action
    if ($this->action === 'deleted' && $this->scheduleId) {
        $data['schedule_id'] = $this->scheduleId;
    }

    return $data;
}
```

**Explanation:**

-   **Channel Name**: `schedule.location.{locationId}` - Location-specific channels for targeted updates
-   **Event Name**: `.schedule.event` - The dot prefix is Laravel convention (becomes `schedule.event` on frontend)
-   **Broadcast Data**: Only includes necessary data based on action type

### Step 3: Format Schedule Data (Optional Helper Method)

**In the same `ScheduleEvent` class:**

```php
/**
 * Format schedule with same structure as index method response
 * Includes carrier_name, trailer_number, door_name, user names, and counts
 */
private function formatScheduleForTable(int $scheduleId): ?Schedule
{
    $tableName = "schedules";
    $careerTableName = "carriers";
    $trailerTableName = "trailers";
    $doorsTableName = "location_doors";

    $schedule = Schedule::select([
            "$tableName.*",
            "$careerTableName.name AS carrier_name",
            "$careerTableName.carrier_type AS carrier_type",
            "$trailerTableName.trailer_number",
            "$doorsTableName.door_name",
        ])
        ->selectRaw("CASE WHEN $tableName.in_time IS NOT NULL THEN in_user.name ELSE NULL END as in_user_name")
        ->selectRaw("CASE WHEN $tableName.out_time IS NOT NULL THEN out_user.name ELSE NULL END as out_user_name")
        ->selectRaw("CASE WHEN $tableName.completed_time IS NOT NULL THEN completed_user.name ELSE NULL END as completed_user_name")
        ->selectRaw("CASE WHEN $tableName.parked_time IS NOT NULL THEN parked_user.name ELSE NULL END as parked_user_name")
        ->leftJoin($careerTableName, function ($join) use ($tableName, $careerTableName) {
            $join->on("$careerTableName.id", '=', "$tableName.carrier_id")
                 ->whereNull("$careerTableName.deleted_at");
        })
        ->leftJoin($trailerTableName, function ($join) use ($tableName, $trailerTableName) {
            $join->on("$trailerTableName.id", '=', "$tableName.trailer_id")
                 ->whereNull("$trailerTableName.deleted_at");
        })
        ->leftJoin("users as in_user", "in_user.id", "$tableName.in_time_by")
        ->leftJoin("users as out_user", "out_user.id", "$tableName.out_time_by")
        ->leftJoin("users as completed_user", "completed_user.id", "$tableName.completed_time_by")
        ->leftJoin("users as parked_user", "parked_user.id", "$tableName.parked_time_by")
        ->leftJoin($doorsTableName, "$doorsTableName.id", "$tableName.door_id")
        ->withCount([
            'scheduledServices AS total_services',
            'scheduledEquipments AS total_equipments'
        ])
        ->where("$tableName.id", $scheduleId)
        ->first();

    return $schedule;
}
```

**Why This Matters:**

-   Ensures frontend receives the same data structure as API responses
-   Includes related data (carrier, trailer, door, user names) in a single broadcast
-   Avoids frontend needing to make additional API calls

### Step 4: Trigger Broadcasting from Model Events

**File:** `app/Models/Schedule.php`

Model events automatically trigger when records are created, updated, or deleted:

```php
protected static function booted()
{
    // Broadcast events for real-time updates
    static::created(function ($schedule) {
        try {
            broadcast(new ScheduleEvent('created', $schedule->location_id, $schedule->id));
        } catch (\Exception $e) {
            // Don't fail the operation if WebSocket server is down
        }
    });

    static::updated(function ($schedule) {
        // Skip broadcasting if only display_order changed
        // Order changes are handled by the order_changed event in changeScheduleOrder()
        $dirtyAttributes = $schedule->getDirty();
        $onlyOrderChanged = count($dirtyAttributes) === 1 && isset($dirtyAttributes['display_order']);
        if ($onlyOrderChanged) {
            return;
        }

        // Check if location_id was changed
        $originalLocationId = $schedule->getOriginal('location_id');
        $newLocationId = $schedule->location_id;
        $locationChanged = $originalLocationId !== null && $originalLocationId != $newLocationId;

        try {
            if ($locationChanged) {
                // Broadcast deleted event to old location
                broadcast(new ScheduleEvent('deleted', $originalLocationId, $schedule->id));
                // Broadcast created event to new location
                broadcast(new ScheduleEvent('created', $newLocationId, $schedule->id));
            } else {
                // Normal update - broadcast to current location only
                broadcast(new ScheduleEvent('updated', $schedule->location_id, $schedule->id));
            }
        } catch (\Exception $e) {
            // Don't fail the operation if WebSocket server is down
        }
    });

    static::deleted(function ($schedule) {
        $locationId = $schedule->location_id;
        $scheduleId = $schedule->id;
        try {
            broadcast(new ScheduleEvent('deleted', $locationId, $scheduleId));
        } catch (\Exception $e) {
            // Don't fail the operation if WebSocket server is down
        }
    });
}
```

**Key Points:**

-   **Error Handling**: Broadcasting failures don't break the main operation
-   **Location Changes**: When a schedule moves to a different location, broadcast to both old and new locations
-   **Order Changes**: Skipped in model events (handled separately in controller)

### Step 5: Manual Broadcasting from Controllers

**File:** `app/Http/Controllers/API/SchedulesController.php`

For operations that don't trigger model events (like bulk updates), broadcast manually:

```php
public function changeScheduleOrder(ScheduleOrderRequest $request)
{
    try {
        $newOrders = $request->validated()['new_orders'];
        $locationId = $request->input('locationId');

        // Bulk update display_order (doesn't trigger model events)
        foreach ($newOrders as $orderData) {
            $query = Schedule::where('id', $orderData['id']);
            if ($locationId) {
                $query->where('location_id', $locationId);
            }
            $query->update(['display_order' => $orderData['order']]);
        }

        // Broadcast event for real-time updates
        // Note: order_changed doesn't need scheduleId parameter
        try {
            broadcast(new ScheduleEvent('order_changed', $locationId));
        } catch (\Exception $e) {
            LoggingHelper::logException($e, "Failed to broadcast schedule order_changed event");
        }

        return ResponseHelper::sendResponse('success', "Schedule display order updated successfully.");
    } catch (\Exception $e) {
        LoggingHelper::logException($e, "Failed to update schedule order");
        return ResponseHelper::sendResponse('error', 'Failed to update.');
    }
}
```

**When to Use Manual Broadcasting:**

-   Bulk operations that bypass model events
-   Custom business logic that needs broadcasting
-   Operations that modify data outside of Eloquent

### Step 6: Configure Channel Authorization

**File:** `routes/channels.php`

Define who can subscribe to which channels:

```php
<?php

use Illuminate\Support\Facades\Broadcast;
use App\Helpers\BroadcastingHelper;

// Allow drivers to listen to their location's schedule channel
Broadcast::channel('schedule.location.{locationId}', function ($user, $locationId) {
    return BroadcastingHelper::canAccessScheduleLocationChannel($user, $locationId);
});
```

**File:** `routes/api.php`

Add authentication endpoint for WebSocket connections:

```php
// WebSocket authentication endpoint
Route::post('/broadcasting/auth', function (Request $request) {
    $user = AuthHelper::getLoginUser();

    if (!$user) {
        return response()->json(['message' => 'Unauthenticated'], 401);
    }

    $channelName = $request->input('channel_name');

    // Validate channel format
    if (!BroadcastingHelper::isValidScheduleChannel($channelName)) {
        return response()->json(['message' => 'Invalid channel'], 400);
    }

    // Extract and validate location ID
    $locationId = BroadcastingHelper::extractLocationIdFromChannel($channelName);
    if ($locationId && !BroadcastingHelper::canAccessScheduleLocationChannel($user, $locationId)) {
        return response()->json(['message' => 'Forbidden'], 403);
    }

    return Broadcast::auth($request);
})->middleware(['throttle:api']);
```

### Step 7: Create Broadcasting Helper

**File:** `app/Helpers/BroadcastingHelper.php`

Centralized authorization logic:

```php
<?php

namespace App\Helpers;

use App\Models\User;

class BroadcastingHelper
{
    /**
     * Check if user can access a schedule location channel
     */
    public static function canAccessScheduleLocationChannel(?User $user, int $locationId): bool
    {
        if (!$user) {
            return false;
        }

        // Drivers can only listen to their assigned location
        if ($user->role === 'driver') {
            $userLocation = $user->locations->first();
            return $userLocation && $userLocation->id == $locationId;
        }

        // Admin and Manager can listen to any location
        return true;
    }

    /**
     * Extract location ID from channel name
     */
    public static function extractLocationIdFromChannel(string $channelName): ?int
    {
        if (preg_match('/^schedule\.location\.(\d+)$/', $channelName, $matches)) {
            return (int) $matches[1];
        }
        return null;
    }

    /**
     * Validate channel name format
     */
    public static function isValidScheduleChannel(string $channelName): bool
    {
        return preg_match('/^schedule\.location\.\d+$/', $channelName) === 1;
    }
}
```

### Step 8: Configure Broadcasting Connection

**File:** `config/broadcasting.php`

```php
'default' => env('BROADCAST_CONNECTION', 'null'),

'connections' => [
    'reverb' => [
        'driver' => 'reverb',
        'key' => SecureEnvHelper::decrypt(env('REVERB_APP_KEY')),
        'secret' => SecureEnvHelper::decrypt(env('REVERB_APP_SECRET')),
        'app_id' => SecureEnvHelper::decrypt(env('REVERB_APP_ID')),
        'options' => [
            // In production, use localhost for HTTP requests
            'host' => env('APP_ENV') === 'production'
                ? (env('REVERB_HTTP_HOST', '127.0.0.1'))
                : env('REVERB_HOST'),
            'port' => env('APP_ENV') === 'production'
                ? (env('REVERB_HTTP_PORT', env('REVERB_SERVER_PORT', 8080)))
                : env('REVERB_PORT', 443),
            'scheme' => env('APP_ENV') === 'production' ? 'http' : env('REVERB_SCHEME', 'https'),
            'useTLS' => env('APP_ENV') === 'production' ? false : (env('REVERB_SCHEME', 'https') === 'https'),
        ],
        'client_options' => [
            // Guzzle HTTP client options for broadcasting HTTP requests
            // Can add timeout, verify SSL, etc. if needed
            // Default options are usually sufficient
        ],
    ],
],
```

**Important Configuration:**

-   **Production**: Uses `localhost:8080` for HTTP broadcasting (same machine)
-   **Development**: Uses public domain/port for testing
-   **Encrypted Values**: Uses `SecureEnvHelper` for decrypting environment variables

**Environment Variables Required:**

```env
# Broadcasting Configuration
BROADCAST_CONNECTION=reverb

# Reverb Configuration (must be encrypted using SecureEnvHelper)
REVERB_APP_ID=encrypted-value-here
REVERB_APP_KEY=encrypted-value-here
REVERB_APP_SECRET=encrypted-value-here

# Reverb Host Configuration
REVERB_HOST=api.premierwarehousingservices.com
REVERB_PORT=8080
REVERB_SCHEME=https

# Reverb Server Configuration (internal)
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080

# Reverb HTTP Broadcasting (optional - for production)
REVERB_HTTP_HOST=127.0.0.1
REVERB_HTTP_PORT=8080

# Redis Configuration
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=your-redis-password
REDIS_PORT=6379
REDIS_DB=0
```

**Encrypting Environment Values:**

This project uses encrypted environment values. After setting the values in `.env`, encrypt them:

```bash
php artisan app:encrypt-env-value REVERB_APP_ID
php artisan app:encrypt-env-value REVERB_APP_KEY
php artisan app:encrypt-env-value REVERB_APP_SECRET
php artisan app:encrypt-env-value REDIS_PASSWORD
```

**Note:** The `SecureEnvHelper::decrypt()` in `config/broadcasting.php` automatically decrypts these values at runtime.

---

## 🔄 Event Flow Diagram

```
1. User Action (Create/Update/Delete Schedule)
   │
   ▼
2. Eloquent Model Event Fires (created/updated/deleted)
   │
   ▼
3. ScheduleEvent Constructor
   ├─ Formats schedule data (if created/updated)
   ├─ Sets action, locationId, scheduleId
   │
   ▼
4. broadcast(new ScheduleEvent(...))
   │
   ▼
5. Laravel Broadcasting System
   ├─ Calls broadcastOn() → Gets channel name
   ├─ Calls broadcastAs() → Gets event name
   ├─ Calls broadcastWith() → Gets data payload
   │
   ▼
6. Reverb Broadcaster
   ├─ Makes HTTP POST to Reverb server
   ├─ Reverb server receives event
   │
   ▼
7. Reverb Server
   ├─ Validates channel exists
   ├─ Sends WebSocket message to subscribed clients
   │
   ▼
8. Frontend Clients
   ├─ Receive WebSocket message
   ├─ Parse event data
   └─ Update UI accordingly
```

---

## 📊 Event Action Types

### 1. `created`

-   **Triggered**: When a new schedule is created
-   **Data Sent**: Full schedule object with all relationships
-   **Frontend Action**: Add new schedule to table/list

### 2. `updated`

-   **Triggered**: When a schedule is updated (except display_order)
-   **Data Sent**: Full updated schedule object
-   **Frontend Action**: Update existing schedule in table/list
-   **Special Case**: If `location_id` changes, sends `deleted` to old location and `created` to new location

### 3. `deleted`

-   **Triggered**: When a schedule is deleted (soft or hard delete)
-   **Data Sent**: Only `schedule_id`
-   **Frontend Action**: Remove schedule from table/list

### 4. `order_changed`

-   **Triggered**: When display order is changed (bulk update)
-   **Data Sent**: Only `action` and `location_id` (no `scheduleId` needed)
-   **Frontend Action**: Refetch entire table to get correct order
-   **Note**: This event is broadcast manually from the controller, not from model events (since bulk updates bypass Eloquent events)

---

## 🔐 Security Considerations

### Channel Authorization

-   **Drivers**: Can only subscribe to their assigned location's channel
-   **Admins/Managers**: Can subscribe to any location's channel
-   **Validation**: Channel name format is validated before authorization

### Authentication

-   WebSocket connections require valid authentication token
-   Token is validated on `/api/broadcasting/auth` endpoint
-   Unauthorized requests return 401/403

### Error Handling

-   Broadcasting failures don't break main operations
-   Exceptions are caught and logged, but don't throw
-   Frontend handles missing/partial data gracefully

---

## 🛠️ Troubleshooting

### Issue: Events Not Broadcasting

**Check:**

1. `BROADCAST_CONNECTION` is set to `reverb` in `.env`
2. Reverb server is running: `pm2 status reverb`
3. Config cache is cleared: `php artisan config:clear`
4. Check Laravel logs for broadcasting errors

### Issue: Channel Authorization Failing

**Check:**

1. User is authenticated (token valid)
2. Channel name format is correct: `schedule.location.{id}`
3. User has permission for that location (check `BroadcastingHelper`)
4. Check `/api/broadcasting/auth` endpoint logs

### Issue: Data Not Serializing

**Check:**

1. Model relationships are properly loaded
2. Hidden attributes don't include required fields
3. Check for circular references in relationships
4. Review `broadcastWith()` method for serialization issues

---

## 📚 Best Practices

1. **Error Handling**: Always wrap `broadcast()` in try-catch to prevent failures from breaking operations
2. **Data Formatting**: Format data in event constructor to match API response structure
3. **Channel Naming**: Use consistent, descriptive channel names (e.g., `resource.entity.{id}`)
4. **Action Types**: Use clear, consistent action names (`created`, `updated`, `deleted`)
5. **Authorization**: Centralize authorization logic in helper classes
6. **Testing**: Test with `BROADCAST_CONNECTION=log` to verify events fire correctly
7. **Performance**: Use `ShouldBroadcastNow` for immediate broadcasts, or queue for heavy operations

---

## 🔄 Adapting for Other Projects

### For a Different Model (e.g., `Order`)

1. **Create Event**: `app/Events/OrderEvent.php`

    ```php
    class OrderEvent implements ShouldBroadcastNow
    {
        public function broadcastOn(): array
        {
            return [new Channel('order.location.' . $this->locationId)];
        }

        public function broadcastAs(): string
        {
            return 'order.event';
        }
    }
    ```

2. **Add Model Events**: In `app/Models/Order.php`

    ```php
    static::created(function ($order) {
        broadcast(new OrderEvent('created', $order->location_id, $order->id));
    });
    ```

3. **Update Channel Authorization**: In `routes/channels.php`

    ```php
    Broadcast::channel('order.location.{locationId}', function ($user, $locationId) {
        return BroadcastingHelper::canAccessLocationChannel($user, $locationId);
    });
    ```

4. **Update Frontend**: Subscribe to `order.location.{locationId}` channel

---

## 📝 Summary

This implementation provides:

-   ✅ Real-time updates without polling
-   ✅ Location-specific channels for targeted updates
-   ✅ Secure channel authorization
-   ✅ Graceful error handling
-   ✅ Consistent data structure with API responses
-   ✅ Reusable patterns for other models
-   ✅ Helper classes for maintainability (BroadcastingHelper)
-   ✅ Comprehensive testing capabilities
-   ✅ Monitoring and health check endpoints

The key is understanding the flow: **Model Event → Broadcast Event → Redis Pub/Sub → Reverb Server → Frontend Client**, with proper authorization and error handling at each step.

### BroadcastingHelper

**File:** `app/Helpers/BroadcastingHelper.php`

This helper class provides reusable methods for WebSocket channel authorization and validation.

**Methods:**

1. **`canAccessScheduleLocationChannel($user, $locationId)`**

    - Checks if a user can access a specific location's channel
    - Drivers can only access their assigned location
    - Admins/Managers can access any location
    - Returns `bool`

2. **`extractLocationIdFromChannel($channelName)`**

    - Extracts location ID from channel name (e.g., `schedule.location.123` → `123`)
    - Uses regex pattern matching
    - Returns `int|null`

3. **`isValidScheduleChannel($channelName)`**

    - Validates channel name format
    - Must match pattern: `schedule.location.{number}`
    - Returns `bool`

4. **`getUserLocationId($user)`**
    - Gets user's location ID based on their role
    - For drivers: returns their assigned location
    - For admins/managers: returns null (location comes from request context)
    - Returns `int|null`

**Usage:**

-   Used in `routes/channels.php` for channel authorization
-   Used in `/api/broadcasting/auth` route for authentication
-   Centralizes authorization logic for reusability

**Benefits:**

-   ✅ Single source of truth for authorization logic
-   ✅ Easy to test and maintain
-   ✅ Reusable across different parts of the application
-   ✅ Consistent authorization behavior

---

## 🧪 Testing

### Testing Broadcasting

**Using Tinker:**

```bash
php artisan tinker

# Test broadcasting a created event
>>> $schedule = App\Models\Schedule::first();
>>> broadcast(new App\Events\ScheduleEvent('created', $schedule->location_id, $schedule->id));

# Test broadcasting an updated event
>>> broadcast(new App\Events\ScheduleEvent('updated', $schedule->location_id, $schedule->id));

# Test broadcasting a deleted event
>>> broadcast(new App\Events\ScheduleEvent('deleted', $schedule->location_id, $schedule->id));

# Test broadcasting order_changed event
>>> broadcast(new App\Events\ScheduleEvent('order_changed', $schedule->location_id));
```

**Using Log Driver (for testing without Reverb):**

Set in `.env`:

```env
BROADCAST_CONNECTION=log
```

Then check `storage/logs/laravel.log` for broadcast events.

### Testing Channel Authorization

```bash
php artisan tinker

# Test channel authorization
>>> $user = App\Models\User::where('role', 'driver')->first();
>>> App\Helpers\BroadcastingHelper::canAccessScheduleLocationChannel($user, 1);
# Should return true if user's location is 1, false otherwise

# Test channel name validation
>>> App\Helpers\BroadcastingHelper::isValidScheduleChannel('schedule.location.1');
# Should return true

>>> App\Helpers\BroadcastingHelper::isValidScheduleChannel('invalid.channel');
# Should return false

# Test location ID extraction
>>> App\Helpers\BroadcastingHelper::extractLocationIdFromChannel('schedule.location.123');
# Should return 123
```

---

## 📊 Monitoring & Health Checks

### Health Check Endpoint

**File:** `routes/api.php`

```php
Route::get('/health/reverb', function () {
    try {
        // Check Redis connection
        Redis::ping();

        // Check if Reverb server is accessible (optional)
        // You can add HTTP check to Reverb server here

        return response()->json([
            'status' => 'ok',
            'redis' => 'connected',
            'timestamp' => now()->toIso8601String(),
        ]);
    } catch (\Exception $e) {
        return response()->json([
            'status' => 'error',
            'message' => $e->getMessage(),
            'timestamp' => now()->toIso8601String(),
        ], 500);
    }
});
```

### Monitoring Logs

**Reverb Logs:**

```bash
# View Reverb logs (if using PM2)
pm2 logs reverb

# View Reverb log files
tail -f storage/logs/reverb-error.log
tail -f storage/logs/reverb-out.log
```

**Laravel Logs:**

```bash
# View Laravel logs
tail -f storage/logs/laravel.log | grep broadcast

# View only broadcasting errors
tail -f storage/logs/laravel.log | grep "Failed to broadcast"
```

**Redis Monitoring:**

```bash
# Monitor Redis commands
redis-cli -a your-password monitor

# Check Redis memory usage
redis-cli -a your-password info memory

# Check connected clients
redis-cli -a your-password info clients
```
