PHP code example of andreagroferreira / laravel-webhook-owlery

1. Go to this page and download the library: Download andreagroferreira/laravel-webhook-owlery library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

andreagroferreira / laravel-webhook-owlery example snippets


'endpoints' => [
    'stripe' => [
        'path' => '/webhooks/stripe',
        'secret' => env('STRIPE_WEBHOOK_SECRET'),
        'validator' => 'hmac',
    ],
],

namespace App\Listeners;

use WizardingCode\WebhookOwlery\Events\WebhookReceived;

class ProcessStripeWebhook
{
    public function handle(WebhookReceived $event)
    {
        $payload = $event->payload;
        $event = $payload['type'] ?? null;
        
        if ($event === 'charge.succeeded') {
            // Process the successful charge
        }
    }
}

protected $listen = [
    \WizardingCode\WebhookOwlery\Events\WebhookReceived::class => [
        \App\Listeners\ProcessStripeWebhook::class,
    ],
];

use WizardingCode\WebhookOwlery\Facades\Owlery;

// Simple webhook
Owlery::send('user.created', ['user' => $user->toArray()]);

// With specific subscriptions
Owlery::to(['subscription-id-1', 'subscription-id-2'])
      ->send('order.shipped', ['order' => $order->toArray()]);

// With metadata
Owlery::withMetadata(['source' => 'backend', 'initiated_by' => 'system'])
      ->send('payment.failed', ['payment' => $payment->toArray()]);

// config/webhook-owlery.php

return [
    'route' => [
        'prefix' => 'api',
        'middleware' => ['api'],
    ],
    
    'signing' => [
        'default' => 'hmac',
        'hmac' => [
            'algorithm' => 'sha256',
            'header' => 'X-Signature',
        ],
    ],
    
    'circuit_breaker' => [
        'enabled' => true,
        'threshold' => 5,
        'recovery_time' => 60,
    ],
    
    // Many more options...
];

'endpoints' => [
    'stripe' => [
        'path' => '/webhooks/stripe',
        'secret' => env('STRIPE_WEBHOOK_SECRET'),
        'validator' => 'hmac',
        'signature_header' => 'Stripe-Signature',
        'rate_limit' => [
            'enabled' => true,
            'attempts' => 60,
            'decay_minutes' => 1,
        ],
    ],
    'github' => [
        'path' => '/webhooks/github',
        'secret' => env('GITHUB_WEBHOOK_SECRET'),
        'validator' => 'hmac',
        'signature_header' => 'X-Hub-Signature-256',
        'rate_limit' => [
            'enabled' => true,
            'attempts' => 60,
            'decay_minutes' => 1,
        ],
    ],
],

return [
    /*
    |--------------------------------------------------------------------------
    | Route Configuration
    |--------------------------------------------------------------------------
    |
    | Configure how webhook routes are registered
    |
    */
    'route' => [
        'prefix' => 'api',
        'middleware' => ['api'],
        'name' => 'webhook',
    ],

    /*
    |--------------------------------------------------------------------------
    | Webhook Database Connection
    |--------------------------------------------------------------------------
    |
    | The database connection to use for webhook storage
    |
    */
    'database' => [
        'connection' => null, // null uses default connection
        'table_prefix' => '',
    ],

    /*
    |--------------------------------------------------------------------------
    | Signing Configuration
    |--------------------------------------------------------------------------
    |
    | Configure how webhooks are signed for validation
    |
    */
    'signing' => [
        'default' => 'hmac',
        'hmac' => [
            'algorithm' => 'sha256',
            'header' => 'X-Signature',
        ],
        'jwt' => [
            'algorithm' => 'HS256',
            'header' => 'Authorization',
            'prefix' => 'Bearer ',
            'leeway' => 60, // seconds
        ],
        'api_key' => [
            'header' => 'X-API-Key',
        ],
        'basic_auth' => [
            'header' => 'Authorization',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Outgoing Webhooks
    |--------------------------------------------------------------------------
    |
    | Configure how webhooks are sent from your application
    |
    */
    'outgoing' => [
        'queue' => [
            'connection' => null, // null uses default connection
            'queue' => 'default',
        ],
        'retry' => [
            'default_attempts' => 3,
            'backoff' => [1, 5, 15], // minutes
            'max_attempts' => 10,
        ],
        'timeout' => 5, // seconds
        'concurrency' => 10, // max concurrent webhook dispatches
        'user_agent' => 'Laravel Webhook Owlery',
        'send_timeout' => true, // -------
    |
    | Configure rate limiting for webhook endpoints
    |
    */
    'rate_limiting' => [
        'enabled' => true,
        'default' => [
            'attempts' => 60,
            'decay_minutes' => 1,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Webhook Endpoints
    |--------------------------------------------------------------------------
    |
    | Define pre-configured webhook endpoints for receiving
    |
    */
    'endpoints' => [
        'stripe' => [
            'path' => '/webhooks/stripe',
            'secret' => env('STRIPE_WEBHOOK_SECRET'),
            'validator' => 'hmac',
            'signature_header' => 'Stripe-Signature',
            'rate_limit' => [
                'enabled' => true,
                'attempts' => 60,
                'decay_minutes' => 1,
            ],
        ],
        'github' => [
            'path' => '/webhooks/github',
            'secret' => env('GITHUB_WEBHOOK_SECRET'),
            'validator' => 'hmac',
            'signature_header' => 'X-Hub-Signature-256',
            'rate_limit' => [
                'enabled' => true,
                'attempts' => 60,
                'decay_minutes' => 1,
            ],
        ],
        'shopify' => [
            'path' => '/webhooks/shopify',
            'secret' => env('SHOPIFY_WEBHOOK_SECRET'),
            'validator' => 'hmac',
            'signature_header' => 'X-Shopify-Hmac-Sha256',
            'rate_limit' => [
                'enabled' => true,
                'attempts' => 60,
                'decay_minutes' => 1,
            ],
        ],
    ],
];

use WizardingCode\WebhookOwlery\Facades\WebhookRepository;

// Create a new webhook endpoint
$endpoint = WebhookRepository::createEndpoint([
    'url' => 'https://example.com/webhook',
    'description' => 'My application webhook endpoint',
    'secret' => Str::random(64),
    'is_active' => true,
]);

// Create a new webhook subscription
$subscription = WebhookRepository::createSubscription([
    'endpoint_id' => $endpoint->id,
    'event_types' => ['user.created', 'user.updated'],
    'description' => 'User events subscription',
    'is_active' => true,
]);

// Update subscription
WebhookRepository::updateSubscription($subscription->id, [
    'event_types' => ['user.created', 'user.updated', 'user.deleted'],
]);

// Deactivate subscription
WebhookRepository::updateSubscription($subscription->id, [
    'is_active' => false,
]);

// Find endpoints and subscriptions
$activeEndpoints = WebhookRepository::getActiveEndpoints();
$userSubscriptions = WebhookRepository::getSubscriptionsByEventType('user.created');

// Get delivery statistics
$stats = WebhookRepository::getDeliveryStatistics();

namespace App\Http\Requests\Webhooks;

use WizardingCode\WebhookOwlery\Http\Requests\WebhookReceiveRequest;

class SlackWebhookRequest extends WebhookReceiveRequest
{
    public function rules()
    {
        return [
            'event' => ' }
    
    public function getEventType()
    {
        return 'slack.' . ($this->input('event.type') ?? 'unknown');
    }
    
    public function getPayload()
    {
        return [
            'event' => $this->input('event'),
            'team_id' => $this->input('team_id'),
            'event_id' => $this->input('event_id'),
            'event_time' => $this->input('event_time'),
            'payload' => $this->input('payload'),
        ];
    }
}

namespace App\Validators;

use WizardingCode\WebhookOwlery\Contracts\SignatureValidatorContract;
use Illuminate\Http\Request;

class SlackSignatureValidator implements SignatureValidatorContract
{
    public function validate(Request $request, string $secret): bool
    {
        $signature = $request->header('X-Slack-Signature');
        $timestamp = $request->header('X-Slack-Request-Timestamp');
        
        // Check if the request is not older than 5 minutes
        $now = time();
        if (abs($now - $timestamp) > 300) {
            return false;
        }
        
        $payload = $request->getContent();
        $baseString = 'v0:' . $timestamp . ':' . $payload;
        
        // Compute signature using the secret
        $computedSignature = 'v0=' . hash_hmac('sha256', $baseString, $secret);
        
        // Compare signatures using constant-time comparison
        return hash_equals($computedSignature, $signature);
    }
}

namespace App\Providers;

use App\Http\Requests\Webhooks\SlackWebhookRequest;
use App\Validators\SlackSignatureValidator;
use WizardingCode\WebhookOwlery\Facades\Owlery;
use WizardingCode\WebhookOwlery\Facades\WebhookReceiver;
use Illuminate\Support\ServiceProvider;

class WebhookServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // Register the signature validator
        Owlery::validator('slack', SlackSignatureValidator::class);
        
        // Register the request handler
        WebhookReceiver::registerProvider('slack', SlackWebhookRequest::class);
    }
}

'endpoints' => [
    // ... other providers
    'slack' => [
        'path' => '/webhooks/slack',
        'secret' => env('SLACK_SIGNING_SECRET'),
        'validator' => 'slack',
        'signature_header' => 'X-Slack-Signature',
        'rate_limit' => [
            'enabled' => true,
            'attempts' => 60,
            'decay_minutes' => 1,
        ],
    ],
],

namespace App\Listeners;

use WizardingCode\WebhookOwlery\Events\WebhookReceived;
use Illuminate\Support\Facades\Log;

class HandleSlackWebhooks
{
    public function handle(WebhookReceived $event)
    {
        if ($event->endpoint !== 'slack') {
            return;
        }
        
        $payload = $event->payload;
        $eventType = $payload['event']['type'] ?? 'unknown';
        
        Log::info('Received Slack webhook', [
            'event_type' => $eventType,
            'team_id' => $payload['team_id'],
        ]);
        
        // Process different Slack event types
        switch ($eventType) {
            case 'message':
                $this->handleMessage($payload);
                break;
                
            case 'app_mention':
                $this->handleAppMention($payload);
                break;
                
            // Handle other event types
        }
    }
    
    protected function handleMessage(array $payload)
    {
        $message = $payload['event']['text'] ?? '';
        $user = $payload['event']['user'] ?? '';
        $channel = $payload['event']['channel'] ?? '';
        
        // Process the message
    }
    
    protected function handleAppMention(array $payload)
    {
        $mention = $payload['event']['text'] ?? '';
        $user = $payload['event']['user'] ?? '';
        $channel = $payload['event']['channel'] ?? '';
        
        // Process the app mention
    }
}

protected $listen = [
    \WizardingCode\WebhookOwlery\Events\WebhookReceived::class => [
        // ... other listeners
        \App\Listeners\HandleSlackWebhooks::class,
    ],
];

// Create a custom validator
namespace App\Validators;

use WizardingCode\WebhookOwlery\Contracts\SignatureValidatorContract;
use Illuminate\Http\Request;

class CustomValidator implements SignatureValidatorContract
{
    public function validate(Request $request, string $secret): bool
    {
        $signature = $request->header('X-Custom-Signature');
        $payload = $request->getContent();
        
        // Your custom validation logic
        $expectedSignature = hash_hmac('sha512', $payload, $secret);
        
        return hash_equals($expectedSignature, $signature);
    }
}

use WizardingCode\WebhookOwlery\Facades\Owlery;

public function boot()
{
    Owlery::validator('custom', \App\Validators\CustomValidator::class);
}

use WizardingCode\WebhookOwlery\Facades\Owlery;

// With custom retry strategy
Owlery::withRetry([
    'attempts' => 5,
    'backoff' => [1, 5, 15, 30, 60], // minutes
])
->send('invoice.paid', ['invoice' => $invoice->toArray()]);

// With conditional recipients
$subscribers = $company->partners->pluck('webhook_subscription_id')->toArray();

Owlery::to($subscribers)
      ->withMetadata([
          'company_id' => $company->id,
          'importance' => 'high',
      ])
      ->send('product.released', ['product' => $product->toArray()]);

// With conditional dispatching
Owlery::when($order->total > 1000)
      ->send('order.high_value', ['order' => $order->toArray()]);

// With custom headers
Owlery::withHeaders([
    'X-Custom-Header' => 'custom-value',
    'X-Source-System' => 'inventory',
])
->send('inventory.updated', ['product' => $product->toArray()]);

// With batch sending
Owlery::batch([
    ['event' => 'user.created', 'payload' => ['user' => $user->toArray()]],
    ['event' => 'profile.created', 'payload' => ['profile' => $profile->toArray()]],
    ['event' => 'preferences.set', 'payload' => ['preferences' => $preferences]],
])
->send();

namespace App\Services;

use App\Models\Order;
use WizardingCode\WebhookOwlery\Facades\Owlery;

class OrderProcessor
{
    public function processOrder(Order $order)
    {
        // Process the order...
        
        // Notify systems about new order
        Owlery::send('order.created', [
            'order' => [
                'id' => $order->id,
                'reference' => $order->reference,
                'total' => $order->total,
                'currency' => $order->currency,
                'status' => $order->status,
                'created_at' => $order->created_at,
            ],
            'customer' => [
                'id' => $order->customer->id,
                'email' => $order->customer->email,
            ],
            'items' => $order->items->map(function ($item) {
                return [
                    'product_id' => $item->product_id,
                    'quantity' => $item->quantity,
                    'price' => $item->price,
                ];
            })->toArray(),
        ]);
        
        // If it's a large order, notify VIP system with high priority
        if ($order->total > 1000) {
            Owlery::to(['vip-orders-subscription'])
                  ->withMetadata([
                      'priority' => 'high',
                      'department' => 'vip-sales',
                  ])
                  ->withRetry([
                      'attempts' => 10,
                      'backoff' => [1, 1, 5, 5, 10, 15, 30, 60, 120, 240],
                  ])
                  ->send('order.vip', [
                      'order' => $order->toArray(),
                      'customer' => $order->customer->toArray(),
                      'sales_rep' => $order->salesRepresentative->toArray(),
                  ]);
        }
    }
    
    public function fulfillOrder(Order $order)
    {
        // Process fulfillment...
        
        // Notify systems about fulfilled order
        Owlery::send('order.fulfilled', [
            'order_id' => $order->id,
            'fulfillment' => [
                'tracking_number' => $order->tracking_number,
                'carrier' => $order->shipping_carrier,
                'fulfilled_at' => now(),
            ],
        ]);
    }
}

namespace App\Services;

use App\Models\User;
use App\Models\Team;
use WizardingCode\WebhookOwlery\Facades\Owlery;

class UserOnboardingService
{
    public function createUser(array $userData)
    {
        // Create the user
        $user = User::create([
            'name' => $userData['name'],
            'email' => $userData['email'],
            'password' => bcrypt($userData['password']),
        ]);
        
        // Send webhook for user creation
        Owlery::send('user.created', [
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
                'created_at' => $user->created_at->toIso8601String(),
            ],
        ]);
        
        return $user;
    }
    
    public function createTeam(User $user, array $teamData)
    {
        // Create the team
        $team = Team::create([
            'name' => $teamData['name'],
            'owner_id' => $user->id,
        ]);
        
        // Associate user with team
        $user->teams()->attach($team->id, ['role' => 'owner']);
        
        // Send webhook for team creation
        Owlery::send('team.created', [
            'team' => [
                'id' => $team->id,
                'name' => $team->name,
                'created_at' => $team->created_at->toIso8601String(),
            ],
            'owner' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
            ],
        ]);
        
        // If this is an enterprise team, notify the sales department
        if (isset($teamData['enterprise']) && $teamData['enterprise']) {
            Owlery::to(['sales-subscription'])
                  ->withMetadata([
                      'priority' => 'high',
                      'department' => 'enterprise-sales',
                  ])
                  ->send('team.enterprise_created', [
                      'team' => $team->toArray(),
                      'owner' => $user->toArray(),
                      'plan' => $teamData['plan'] ?? 'free',
                  ]);
        }
        
        return $team;
    }
    
    public function completeOnboarding(User $user, Team $team)
    {
        // Update user and team status
        $user->update(['onboarded_at' => now()]);
        $team->update(['setup_completed_at' => now()]);
        
        // Send webhook for completed onboarding
        Owlery::send('onboarding.completed', [
            'user' => [
                'id' => $user->id,
                'name' => $user->name,
                'email' => $user->email,
            ],
            'team' => [
                'id' => $team->id,
                'name' => $team->name,
            ],
            'completed_at' => now()->toIso8601String(),
        ]);
        
        // Notify multiple systems about the completed onboarding
        Owlery::to(['marketing-subscription', 'customer-success-subscription'])
              ->withMetadata([
                  'source' => 'onboarding-service',
                  'completion_time' => round((now()->timestamp - $user->created_at->timestamp) / 60), // minutes
              ])
              ->send('user.onboarded', [
                  'user' => $user->toArray(),
                  'team' => $team->toArray(),
                  'journey' => [
                      'started_at' => $user->created_at->toIso8601String(),
                      'completed_at' => now()->toIso8601String(),
                      'duration_minutes' => round((now()->timestamp - $user->created_at->timestamp) / 60),
                  ],
              ]);
    }
}

namespace App\Listeners;

use WizardingCode\WebhookOwlery\Events\WebhookReceived;
use App\Models\Payment;
use App\Services\SubscriptionService;
use Illuminate\Support\Facades\Log;

class HandleStripeWebhooks
{
    protected $subscriptionService;
    
    public function __construct(SubscriptionService $subscriptionService)
    {
        $this->subscriptionService = $subscriptionService;
    }
    
    public function handle(WebhookReceived $event)
    {
        if ($event->endpoint !== 'stripe') {
            return;
        }
        
        $payload = $event->payload;
        $stripeEvent = $payload['type'] ?? null;
        
        switch ($stripeEvent) {
            case 'invoice.payment_succeeded':
                $this->handleInvoicePaymentSucceeded($payload);
                break;
                
            case 'customer.subscription.deleted':
                $this->handleSubscriptionCancelled($payload);
                break;
                
            case 'payment_intent.payment_failed':
                $this->handlePaymentFailed($payload);
                break;
                
            default:
                Log::info('Unhandled Stripe webhook', ['type' => $stripeEvent]);
        }
    }
    
    protected function handleInvoicePaymentSucceeded(array $payload)
    {
        $invoiceData = $payload['data']['object'];
        $customerId = $invoiceData['customer'];
        $subscriptionId = $invoiceData['subscription'];
        
        // Record the payment
        Payment::create([
            'stripe_customer_id' => $customerId,
            'stripe_subscription_id' => $subscriptionId,
            'amount' => $invoiceData['amount_paid'],
            'currency' => $invoiceData['currency'],
            'invoice_id' => $invoiceData['id'],
            'status' => 'succeeded',
        ]);
        
        // Update the subscription
        $this->subscriptionService->extendSubscription($subscriptionId);
    }
    
    protected function handleSubscriptionCancelled(array $payload)
    {
        $subscriptionData = $payload['data']['object'];
        $subscriptionId = $subscriptionData['id'];
        
        $this->subscriptionService->cancelSubscription($subscriptionId);
    }
    
    protected function handlePaymentFailed(array $payload)
    {
        $paymentData = $payload['data']['object'];
        $customerId = $paymentData['customer'];
        
        // Record the failed payment
        Payment::create([
            'stripe_customer_id' => $customerId,
            'amount' => $paymentData['amount'],
            'currency' => $paymentData['currency'],
            'payment_intent_id' => $paymentData['id'],
            'status' => 'failed',
            'error_message' => $paymentData['last_payment_error']['message'] ?? null,
        ]);
        
        // Notify customer about failed payment
        $this->subscriptionService->notifyPaymentFailure($customerId);
    }
}

namespace App\Services;

use WizardingCode\WebhookOwlery\Facades\Owlery;
use WizardingCode\WebhookOwlery\Facades\WebhookRepository;
use Illuminate\Support\Collection;

class EnhancedWebhookDispatcher
{
    /**
     * Send webhooks to subscriptions based on tags
     */
    public function sendToTags(array $tags, string $eventType, array $payload, array $options = [])
    {
        // Find all subscriptions with the given tags
        $subscriptions = WebhookRepository::getSubscriptionsByTags($tags);
        
        if ($subscriptions->isEmpty()) {
            return;
        }
        
        return $this->dispatchToSubscriptions($subscriptions, $eventType, $payload, $options);
    }
    
    /**
     * Send webhooks to a subset of subscriptions based on a filter
     */
    public function sendToFiltered(callable $filter, string $eventType, array $payload, array $options = [])
    {
        // Get all subscriptions for the event type
        $allSubscriptions = WebhookRepository::getSubscriptionsByEventType($eventType);
        
        // Filter the subscriptions
        $filteredSubscriptions = $allSubscriptions->filter($filter);
        
        if ($filteredSubscriptions->isEmpty()) {
            return;
        }
        
        return $this->dispatchToSubscriptions($filteredSubscriptions, $eventType, $payload, $options);
    }
    
    /**
     * Send feature announcements to all active subscriptions
     */
    public function announceFeature(string $featureName, array $featureDetails, array $options = [])
    {
        // Prepare the payload
        $payload = [
            'feature' => $featureName,
            'details' => $featureDetails,
            'announced_at' => now()->toIso8601String(),
        ];
        
        // Set default options
        $options = array_merge([
            'retry' => [
                'attempts' => 5,
                'backoff' => [5, 15, 30, 60, 120], // minutes
            ],
            'metadata' => [
                'importance' => 'announcement',
                'source' => 'product-team',
            ],
        ], $options);
        
        // Get all active subscriptions
        $activeSubscriptions = WebhookRepository::getActiveSubscriptions();
        
        return $this->dispatchToSubscriptions($activeSubscriptions, 'feature.announced', $payload, $options);
    }
    
    /**
     * Dispatch webhooks to multiple subscriptions with options
     */
    protected function dispatchToSubscriptions(Collection $subscriptions, string $eventType, array $payload, array $options = [])
    {
        $subscriptionIds = $subscriptions->pluck('id')->toArray();
        
        $dispatcher = Owlery::to($subscriptionIds);
        
        // Apply retry strategy if provided
        if (isset($options['retry'])) {
            $dispatcher->withRetry($options['retry']);
        }
        
        // Apply metadata if provided
        if (isset($options['metadata'])) {
            $dispatcher->withMetadata($options['metadata']);
        }
        
        // Apply headers if provided
        if (isset($options['headers'])) {
            $dispatcher->withHeaders($options['headers']);
        }
        
        return $dispatcher->send($eventType, $payload);
    }
}

// Send to subscriptions with specific tags
$dispatcher = new EnhancedWebhookDispatcher();
$dispatcher->sendToTags(['billing', 'finance'], 'invoice.paid', [
    'invoice' => $invoice->toArray(),
]);

// Send to filtered subscriptions
$dispatcher->sendToFiltered(function ($subscription) use ($user) {
    // Only send to subscriptions belonging to the user's organization
    return $subscription->organization_id === $user->organization_id;
}, 'project.created', [
    'project' => $project->toArray(),
]);

// Announce a new feature
$dispatcher->announceFeature('Advanced Analytics', [
    'name' => 'Advanced Analytics',
    'description' => 'Track detailed metrics with our new analytics dashboard',
    'documentation_url' => 'https://docs.example.com/advanced-analytics',
    'available_from' => now()->addDays(7)->toIso8601String(),
]);

namespace Tests\Feature;

use WizardingCode\WebhookOwlery\Facades\Owlery;
use WizardingCode\WebhookOwlery\Events\WebhookReceived;
use WizardingCode\WebhookOwlery\Facades\WebhookRepository;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;

class WebhookTest extends TestCase
{
    /** @test */
    public function it_can_receive_and_validate_webhooks()
    {
        Event::fake([WebhookReceived::class]);
        
        $payload = [
            'id' => 'evt_123456',
            'type' => 'payment_intent.succeeded',
            'data' => [
                'object' => [
                    'id' => 'pi_123456',
                    'amount' => 1500,
                    'currency' => 'usd',
                ],
            ],
        ];
        
        // Generate a signature for the webhook
        $secret = config('webhook-owlery.endpoints.stripe.secret');
        $timestamp = time();
        $signature = $timestamp . '.' . hash_hmac('sha256', $timestamp . '.' . json_encode($payload), $secret);
        
        // Make the request
        $response = $this->postJson('/api/webhooks/stripe', $payload, [
            'Stripe-Signature' => $signature,
        ]);
        
        $response->assertStatus(200);
        
        // Assert the event was dispatched
        Event::assertDispatched(WebhookReceived::class, function ($event) {
            return $event->endpoint === 'stripe' && 
                   $event->payload['type'] === 'payment_intent.succeeded';
        });
    }
    
    /** @test */
    public function it_rejects_webhooks_with_invalid_signatures()
    {
        $payload = [
            'id' => 'evt_123456',
            'type' => 'payment_intent.succeeded',
        ];
        
        // Make the request with an invalid signature
        $response = $this->postJson('/api/webhooks/stripe', $payload, [
            'Stripe-Signature' => 'invalid-signature',
        ]);
        
        $response->assertStatus(403);
    }
    
    /** @test */
    public function it_can_send_webhooks()
    {
        // Mock the HTTP client
        Http::fake([
            '*' => Http::response(['success' => true], 200),
        ]);
        
        // Create a test endpoint
        $endpoint = WebhookRepository::createEndpoint([
            'url' => 'https://example.com/webhook',
            'secret' => 'test-secret',
            'is_active' => true,
            'name' => 'Test Endpoint',
            'source' => 'testing',
        ]);
        
        // Create a test subscription
        $subscription = WebhookRepository::createSubscription([
            'endpoint_id' => $endpoint->id,
            'event_types' => ['test.event'],
            'is_active' => true,
        ]);
        
        // Send the webhook
        $result = Owlery::to([$subscription->id])
                        ->send('test.event', ['message' => 'Hello, world!']);
        
        // Assert the webhook was sent
        Http::assertSent(function ($request) {
            return $request->url() === 'https://example.com/webhook' && 
                   $request->hasHeader('X-Signature') &&
                   json_decode($request->body(), true)['event'] === 'test.event';
        });
    }
}

// Get webhook delivery summary
$summary = DB::table('webhook_deliveries')
    ->select(
        DB::raw('DATE(created_at) as date'),
        DB::raw('COUNT(*) as total'),
        DB::raw('SUM(CASE WHEN status = "success" THEN 1 ELSE 0 END) as successful'),
        DB::raw('SUM(CASE WHEN status = "failed" THEN 1 ELSE 0 END) as failed'),
        DB::raw('AVG(CASE WHEN status = "success" THEN response_time ELSE NULL END) as avg_response_time')
    )
    ->where('created_at', '>=', now()->subDays(30))
    ->groupBy('date')
    ->orderBy('date', 'desc')
    ->get();

// Get most frequent webhook events
$topEvents = DB::table('webhook_events')
    ->select('type', DB::raw('COUNT(*) as count'))
    ->where('created_at', '>=', now()->subDays(30))
    ->groupBy('type')
    ->orderBy('count', 'desc')
    ->limit(10)
    ->get();

// Get endpoints with most failures
$problematicEndpoints = DB::table('webhook_deliveries')
    ->join('webhook_subscriptions', 'webhook_deliveries.subscription_id', '=', 'webhook_subscriptions.id')
    ->join('webhook_endpoints', 'webhook_subscriptions.endpoint_id', '=', 'webhook_endpoints.id')
    ->select(
        'webhook_endpoints.url',
        DB::raw('COUNT(*) as total_attempts'),
        DB::raw('SUM(CASE WHEN webhook_deliveries.status = "failed" THEN 1 ELSE 0 END) as failed'),
        DB::raw('(SUM(CASE WHEN webhook_deliveries.status = "failed" THEN 1 ELSE 0 END) / COUNT(*)) * 100 as failure_rate')
    )
    ->where('webhook_deliveries.created_at', '>=', now()->subDays(30))
    ->groupBy('webhook_endpoints.url')
    ->having('total_attempts', '>', 10)
    ->orderBy('failure_rate', 'desc')
    ->limit(10)
    ->get();
bash
php artisan vendor:publish --provider="WizardingCode\WebhookOwlery\WebhookOwleryServiceProvider"
php artisan migrate
bash
composer 
bash
php artisan webhook:generate-secret
bash
php artisan webhook:list-endpoints
bash
php artisan webhook:cleanup