New updates
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace App\Services\Payments;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use RuntimeException;
|
||||
|
||||
final class LemonSqueezyWebhookVerifier implements WebhookVerifier
|
||||
{
|
||||
public function verify(Request $request, string $rawBody): void
|
||||
{
|
||||
$secret = (string) config('rsync-studio.lemon_squeezy.webhook_secret');
|
||||
$signature = (string) $request->header('X-Signature');
|
||||
if ($secret === '' || $signature === '') throw new RuntimeException('Lemon Squeezy webhook verification is not configured.');
|
||||
$expected = hash_hmac('sha256',$rawBody,$secret);
|
||||
if (!hash_equals($expected,$signature)) throw new RuntimeException('Lemon Squeezy webhook signature is invalid.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace App\Services\Payments;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use RuntimeException;
|
||||
|
||||
final class PaddleWebhookVerifier implements WebhookVerifier
|
||||
{
|
||||
public function verify(Request $request, string $rawBody): void
|
||||
{
|
||||
$secret = (string) config('rsync-studio.paddle.webhook_secret');
|
||||
$header = (string) $request->header('Paddle-Signature');
|
||||
if ($secret === '' || $header === '') throw new RuntimeException('Paddle webhook verification is not configured.');
|
||||
$parts = [];
|
||||
foreach (explode(';', $header) as $part) { [$k,$v] = array_pad(explode('=',trim($part),2),2,null); if ($k && $v) $parts[$k][]=$v; }
|
||||
$ts = isset($parts['ts'][0]) ? (int)$parts['ts'][0] : 0;
|
||||
$signatures = $parts['h1'] ?? [];
|
||||
$tolerance = max(1,(int)config('rsync-studio.paddle.timestamp_tolerance',300));
|
||||
if (!$ts || abs(time()-$ts)>$tolerance) throw new RuntimeException('Paddle webhook timestamp is outside the allowed tolerance.');
|
||||
$expected = hash_hmac('sha256', $ts.':'.$rawBody, $secret);
|
||||
foreach ($signatures as $signature) if (hash_equals($expected,$signature)) return;
|
||||
throw new RuntimeException('Paddle webhook signature is invalid.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace App\Services\Payments;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseProduct;
|
||||
use App\Services\Licensing\LicenseIssuer;
|
||||
use RuntimeException;
|
||||
|
||||
final class PurchaseProvisioner
|
||||
{
|
||||
public function __construct(private LicenseIssuer $issuer) {}
|
||||
|
||||
public function provision(string $provider, array $event): void
|
||||
{
|
||||
$normalized = match ($provider) {
|
||||
'paddle' => $this->fromPaddle($event),
|
||||
'lemon-squeezy' => $this->fromLemonSqueezy($event),
|
||||
default => throw new RuntimeException('Unsupported payment provider.'),
|
||||
};
|
||||
if ($normalized['action'] === 'ignore') return;
|
||||
if ($normalized['action'] === 'revoke') {
|
||||
License::where('payment_provider',$provider)->where('external_order_id',$normalized['orderID'])->whereNull('revoked_at')->update(['revoked_at'=>now(),'revoke_reason'=>'Payment refunded or reversed']);
|
||||
return;
|
||||
}
|
||||
$product = LicenseProduct::where('payment_provider',$provider)->where(function($q) use($normalized){ $q->where('external_price_id',$normalized['priceID'])->orWhere('external_product_id',$normalized['productID']); })->where('active',true)->first();
|
||||
if (!$product) throw new RuntimeException('No active licence product maps to this payment item.');
|
||||
$this->issuer->issue($product,$normalized['name'],$normalized['email'],$provider,$normalized['orderID'],$normalized['customerID'], true);
|
||||
}
|
||||
|
||||
private function fromPaddle(array $e): array
|
||||
{
|
||||
$type=(string)($e['event_type']??''); $d=$e['data']??[];
|
||||
if (in_array($type,['transaction.refunded','transaction.canceled'],true)) return ['action'=>'revoke','orderID'=>(string)($d['id']??''),'priceID'=>'','productID'=>'','name'=>null,'email'=>null,'customerID'=>null];
|
||||
if ($type!=='transaction.completed') return ['action'=>'ignore'];
|
||||
$item=$d['items'][0]??[]; $price=$item['price']??[]; $product=$price['product_id']??($item['product_id']??null);
|
||||
return ['action'=>'issue','orderID'=>(string)($d['id']??''),'priceID'=>(string)($price['id']??($item['price_id']??'')),'productID'=>(string)($product??''),'name'=>$d['custom_data']['customer_name']??null,'email'=>$d['custom_data']['customer_email']??null,'customerID'=>isset($d['customer_id'])?(string)$d['customer_id']:null];
|
||||
}
|
||||
|
||||
private function fromLemonSqueezy(array $e): array
|
||||
{
|
||||
$type=(string)($e['meta']['event_name']??''); $a=$e['data']['attributes']??[];
|
||||
$order=(string)($e['data']['id']??($a['order_id']??''));
|
||||
if (in_array($type,['order_refunded','subscription_expired'],true)) return ['action'=>'revoke','orderID'=>$order,'priceID'=>'','productID'=>'','name'=>null,'email'=>null,'customerID'=>null];
|
||||
if (!in_array($type,['order_created','subscription_created'],true)) return ['action'=>'ignore'];
|
||||
return ['action'=>'issue','orderID'=>$order,'priceID'=>(string)($a['variant_id']??''),'productID'=>(string)($a['product_id']??''),'name'=>$a['user_name']??null,'email'=>$a['user_email']??null,'customerID'=>isset($a['customer_id'])?(string)$a['customer_id']:null];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace App\Services\Payments;
|
||||
use Illuminate\Http\Request;
|
||||
interface WebhookVerifier { public function verify(Request $request, string $rawBody): void; }
|
||||
Reference in New Issue
Block a user