Certified/app/Http/Controllers/MoneybirdWebhookController.php
2026-08-09 16:33:53 +02:00

47 lines
1.8 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Services\Billing\SubscriptionManager;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
/**
* Receives Moneybird's webhook POSTs (see `php artisan moneybird:setup`).
* Moneybird includes the webhook's `webhook_token` in every delivery so we
* can confirm it's genuinely from the subscription we registered — that's
* verified with a constant-time comparison rather than an HMAC signature,
* since Moneybird's webhook docs describe a shared token, not a signed
* payload.
*
* The exact POST field names below (`webhook_token`, `entity_type`,
* `entity_id`) are Moneybird's documented webhook-delivery fields, but
* weren't independently confirmed against a live delivery while building
* this — the raw payload is logged so that's easy to double-check the
* first time a real event comes in, and the daily `app:sync-subscriptions`
* sweep means nothing breaks even if this needs adjusting.
*/
class MoneybirdWebhookController extends Controller
{
public function __invoke(Request $request, SubscriptionManager $subscriptions): Response
{
Log::info('Moneybird webhook received.', $request->all());
$expected = (string) config('services.moneybird.webhook_token');
$received = (string) $request->input('webhook_token', '');
if (blank($expected) || blank($received) || ! hash_equals($expected, $received)) {
abort(403);
}
$entityType = $request->input('entity_type');
$entityId = $request->input('entity_id');
if ($entityType === 'SalesInvoice' && filled($entityId)) {
$subscriptions->handleSalesInvoiceEvent((string) $entityId);
}
return response()->noContent();
}
}