45 lines
2.9 KiB
PHP
45 lines
2.9 KiB
PHP
<?php
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\LicenseWebhookEvent;
|
|
use App\Services\Payments\LemonSqueezyWebhookVerifier;
|
|
use App\Services\Payments\PaddleWebhookVerifier;
|
|
use App\Services\Payments\PurchaseProvisioner;
|
|
use App\Services\Licensing\LicenseDeliveryService;
|
|
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]);
|
|
}
|
|
}
|