79 lines
3.4 KiB
PHP
Executable File
79 lines
3.4 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\LicenseWebhookEvent;
|
|
use App\Services\Licensing\LicenseDeliveryService;
|
|
use App\Services\Payments\LemonSqueezyWebhookVerifier;
|
|
use App\Services\Payments\PaddleWebhookVerifier;
|
|
use App\Services\Payments\PurchaseProvisioner;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class PaymentWebhookController extends Controller
|
|
{
|
|
public function __invoke(string $provider, Request $request, PurchaseProvisioner $provisioner, LicenseDeliveryService $delivery): JsonResponse
|
|
{
|
|
$raw = (string) $request->getContent();
|
|
$verifier = match ($provider) {
|
|
'paddle' => app(PaddleWebhookVerifier::class),'lemon-squeezy' => app(LemonSqueezyWebhookVerifier::class),default => null
|
|
};
|
|
if (! $verifier) {
|
|
return response()->json(['message' => 'Unknown webhook provider.'], 404);
|
|
}
|
|
try {
|
|
$verifier->verify($request, $raw);
|
|
} catch (\Throwable $e) {
|
|
report($e);
|
|
|
|
return response()->json(['message' => 'Invalid webhook signature.'], 400);
|
|
}
|
|
try {
|
|
$payload = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
|
|
} catch (\Throwable) {
|
|
return response()->json(['message' => 'Invalid JSON.'], 400);
|
|
}
|
|
$externalID = (string) ($provider === 'paddle' ? ($payload['event_id'] ?? '') : ($payload['meta']['event_name'] ?? '').':'.($payload['data']['id'] ?? ''));
|
|
$eventType = (string) ($provider === 'paddle' ? ($payload['event_type'] ?? 'unknown') : ($payload['meta']['event_name'] ?? 'unknown'));
|
|
if ($externalID === '') {
|
|
return response()->json(['message' => 'Webhook event has no stable identifier.'], 400);
|
|
}
|
|
|
|
try {
|
|
DB::transaction(function () use ($provider, $externalID, $eventType, $payload, $raw, $provisioner) {
|
|
$event = LicenseWebhookEvent::firstOrCreate(['provider' => $provider, 'external_event_id' => $externalID], ['event_type' => $eventType, 'payload_sha256' => hash('sha256', $raw), 'payload' => $payload]);
|
|
if ($event->processed_at) {
|
|
return;
|
|
}
|
|
if (! hash_equals($event->payload_sha256, hash('sha256', $raw))) {
|
|
throw new \RuntimeException('Webhook ID was reused with a different payload.');
|
|
}
|
|
try {
|
|
$provisioner->provision($provider, $payload);
|
|
$event->update(['processed_at' => now(), 'processing_error' => null]);
|
|
} catch (\Throwable $e) {
|
|
$event->update(['processing_error' => mb_substr($e->getMessage(), 0, 4000)]);
|
|
throw $e;
|
|
}
|
|
});
|
|
} catch (\Throwable $e) {
|
|
report($e);
|
|
|
|
return response()->json(['message' => 'Webhook could not be processed.'], 500);
|
|
}
|
|
|
|
// Delivery is intentionally outside the webhook transaction. A temporary mail
|
|
// outage must not roll back an otherwise valid Paddle event. Pending outbox
|
|
// rows remain available for portal resend or a later webhook delivery attempt.
|
|
try {
|
|
$delivery->deliverPending(10);
|
|
} catch (\Throwable $e) {
|
|
report($e);
|
|
}
|
|
|
|
return response()->json(['ok' => true]);
|
|
}
|
|
}
|