update to provisioner code
This commit is contained in:
@@ -11,6 +11,8 @@ use App\Services\Payments\PurchaseProvisioner;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
final class PaymentWebhookController extends Controller
|
||||
{
|
||||
@@ -36,7 +38,7 @@ final class PaymentWebhookController extends Controller
|
||||
|
||||
try {
|
||||
$verifier->verify($request, $raw);
|
||||
} catch (\Throwable $e) {
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json([
|
||||
@@ -50,7 +52,7 @@ final class PaymentWebhookController extends Controller
|
||||
true,
|
||||
flags: JSON_THROW_ON_ERROR
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
} catch (Throwable) {
|
||||
return response()->json([
|
||||
'message' => 'Invalid JSON.',
|
||||
], 400);
|
||||
@@ -76,16 +78,16 @@ final class PaymentWebhookController extends Controller
|
||||
], 400);
|
||||
}
|
||||
|
||||
$payloadHash = hash('sha256', $raw);
|
||||
|
||||
try {
|
||||
/*
|
||||
* Paddle can deliver several related webhook events at almost
|
||||
* exactly the same time.
|
||||
* Paddle can deliver several related events concurrently.
|
||||
*
|
||||
* Laravel will automatically retry the entire transaction when
|
||||
* MySQL reports a deadlock / serialization failure.
|
||||
*
|
||||
* Five attempts gives short-lived webhook contention time to
|
||||
* resolve without weakening any database constraints.
|
||||
* Laravel retries this complete transaction when MySQL reports
|
||||
* a deadlock. Five attempts gives short-lived lock contention
|
||||
* enough opportunity to resolve without acknowledging an event
|
||||
* that was not actually processed.
|
||||
*/
|
||||
DB::transaction(
|
||||
function () use (
|
||||
@@ -93,15 +95,14 @@ final class PaymentWebhookController extends Controller
|
||||
$externalID,
|
||||
$eventType,
|
||||
$payload,
|
||||
$raw,
|
||||
$payloadHash,
|
||||
$provisioner
|
||||
): void {
|
||||
/*
|
||||
* firstOrCreate protects against Paddle retrying the exact
|
||||
* same event ID.
|
||||
* Insert the event if this is its first delivery.
|
||||
*
|
||||
* lockForUpdate prevents two workers handling the same
|
||||
* webhook event concurrently after the row exists.
|
||||
* Once present, lock the row so duplicate deliveries of
|
||||
* the same event cannot provision concurrently.
|
||||
*/
|
||||
LicenseWebhookEvent::firstOrCreate(
|
||||
[
|
||||
@@ -110,7 +111,7 @@ final class PaymentWebhookController extends Controller
|
||||
],
|
||||
[
|
||||
'event_type' => $eventType,
|
||||
'payload_sha256' => hash('sha256', $raw),
|
||||
'payload_sha256' => $payloadHash,
|
||||
'payload' => $payload,
|
||||
]
|
||||
);
|
||||
@@ -121,27 +122,15 @@ final class PaymentWebhookController extends Controller
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
|
||||
/*
|
||||
* Already successfully processed.
|
||||
*
|
||||
* Returning here makes duplicate delivery of the exact
|
||||
* same Paddle event idempotent.
|
||||
*/
|
||||
if ($event->processed_at !== null) {
|
||||
if ($event->processed_at) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* An external event ID must never be reused with different
|
||||
* content.
|
||||
*/
|
||||
$incomingHash = hash('sha256', $raw);
|
||||
|
||||
if (! hash_equals(
|
||||
(string) $event->payload_sha256,
|
||||
$incomingHash
|
||||
$payloadHash
|
||||
)) {
|
||||
throw new \RuntimeException(
|
||||
throw new RuntimeException(
|
||||
'Webhook ID was reused with a different payload.'
|
||||
);
|
||||
}
|
||||
@@ -152,31 +141,30 @@ final class PaymentWebhookController extends Controller
|
||||
$payload
|
||||
);
|
||||
|
||||
$event->update([
|
||||
'processed_at' => now(),
|
||||
'processing_error' => null,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
$event->processed_at = now();
|
||||
$event->processing_error = null;
|
||||
$event->save();
|
||||
} catch (Throwable $e) {
|
||||
/*
|
||||
* This update is part of the same transaction.
|
||||
*
|
||||
* If processing fails and the transaction rolls back,
|
||||
* Paddle can retry the webhook normally.
|
||||
* This assignment is useful when the surrounding
|
||||
* transaction eventually succeeds. If processing
|
||||
* throws, the transaction itself is rolled back and
|
||||
* Paddle will retry the event.
|
||||
*/
|
||||
$event->update([
|
||||
'processing_error' => mb_substr(
|
||||
$e->getMessage(),
|
||||
0,
|
||||
4000
|
||||
),
|
||||
]);
|
||||
$event->processing_error = mb_substr(
|
||||
$e->getMessage(),
|
||||
0,
|
||||
4000
|
||||
);
|
||||
|
||||
$event->save();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
},
|
||||
5
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json([
|
||||
@@ -189,15 +177,15 @@ final class PaymentWebhookController extends Controller
|
||||
}
|
||||
|
||||
/*
|
||||
* Delivery is intentionally outside the webhook transaction.
|
||||
* Delivery deliberately happens outside the webhook transaction.
|
||||
*
|
||||
* A temporary mail outage must not roll back an otherwise valid
|
||||
* payment event. Pending outbox rows remain available for portal
|
||||
* resend or a later webhook delivery attempt.
|
||||
* A temporary SMTP failure must not undo a successfully processed
|
||||
* payment event. The outbox retains anything that still needs
|
||||
* delivery.
|
||||
*/
|
||||
try {
|
||||
$delivery->deliverPending(10);
|
||||
} catch (\Throwable $e) {
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user