cod consolidation and style updates

This commit is contained in:
Mark Gregory
2026-09-10 09:41:59 +01:00
parent 2648a8f5b5
commit 8dd7d76b8e
35 changed files with 500 additions and 148 deletions
@@ -1,4 +1,5 @@
<?php
namespace App\Services\Payments;
use Illuminate\Http\Request;
@@ -10,8 +11,12 @@ final class LemonSqueezyWebhookVerifier implements WebhookVerifier
{
$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.');
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.');
}
}
}
+9 -4
View File
@@ -10,12 +10,17 @@ final class PaddleClient
{
public function customer(?string $customerID): ?array
{
if (!$customerID || !config('rsync-studio.paddle.api_key')) return null;
if (! $customerID || ! config('rsync-studio.paddle.api_key')) {
return null;
}
$response = $this->request()->get('/customers/'.$customerID);
if (!$response->successful()) return null;
if (! $response->successful()) {
return null;
}
$data = $response->json('data');
return is_array($data) ? $data : null;
}
@@ -62,13 +67,13 @@ final class PaddleClient
private function subscriptionData(Response $response, string $fallback): array
{
if (!$response->successful()) {
if (! $response->successful()) {
$detail = trim((string) ($response->json('error.detail') ?? ''));
throw new RuntimeException($detail !== '' ? $detail : $fallback);
}
$data = $response->json('data');
if (!is_array($data)) {
if (! is_array($data)) {
throw new RuntimeException($fallback);
}
@@ -1,4 +1,5 @@
<?php
namespace App\Services\Payments;
use Illuminate\Http\Request;
@@ -10,15 +11,28 @@ final class PaddleWebhookVerifier implements WebhookVerifier
{
$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.');
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;
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.');
$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;
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) {
return;
}
}
throw new RuntimeException('Paddle webhook signature is invalid.');
}
}
+14 -4
View File
@@ -16,11 +16,13 @@ final class PurchaseProvisioner
{
if ($provider === 'paddle') {
$this->provisionPaddle($event);
return;
}
if ($provider === 'lemon-squeezy') {
$this->provisionLemonSqueezy($event);
return;
}
@@ -34,11 +36,13 @@ final class PurchaseProvisioner
if (str_starts_with($type, 'subscription.')) {
$this->syncPaddleSubscription($type, $data);
return;
}
if ($type === 'transaction.completed') {
$this->syncPaddleTransaction($data);
return;
}
@@ -71,7 +75,7 @@ final class PurchaseProvisioner
->where('external_subscription_id', $subscriptionID)
->first();
if (!$license) {
if (! $license) {
$product = $this->mappedProduct('paddle', $priceID, $productID);
$custom = is_array($data['custom_data'] ?? null) ? $data['custom_data'] : [];
$customer = $this->paddle->customer($this->nullableString($data['customer_id'] ?? null));
@@ -136,6 +140,7 @@ final class PurchaseProvisioner
->where('external_subscription_id', $subscriptionID)
->whereNull('external_order_id')
->update(['external_order_id' => $transactionID ?: null]);
return;
}
@@ -170,10 +175,11 @@ final class PurchaseProvisioner
->where('external_order_id', $order)
->whereNull('revoked_at')
->update(['revoked_at' => now(), 'revoke_reason' => 'Payment refunded or subscription expired']);
return;
}
if (!in_array($type, ['order_created', 'subscription_created'], true)) {
if (! in_array($type, ['order_created', 'subscription_created'], true)) {
return;
}
@@ -210,7 +216,7 @@ final class PurchaseProvisioner
->first();
}
if (!$product) {
if (! $product) {
throw new RuntimeException("No active licence product maps to {$provider} price {$priceID} / product {$productID}.");
}
@@ -224,13 +230,17 @@ final class PurchaseProvisioner
return $item;
}
}
return $items[0] ?? [];
}
private function nullableString(mixed $value): ?string
{
if (!is_scalar($value)) return null;
if (! is_scalar($value)) {
return null;
}
$value = trim((string) $value);
return $value === '' ? null : $value;
}
}
+7 -1
View File
@@ -1,4 +1,10 @@
<?php
namespace App\Services\Payments;
use Illuminate\Http\Request;
interface WebhookVerifier { public function verify(Request $request, string $rawBody): void; }
interface WebhookVerifier
{
public function verify(Request $request, string $rawBody): void;
}