Add PayPal checkout provider

This commit is contained in:
Codex Agent
2026-02-04 12:18:14 +01:00
parent 56a39d0535
commit fc5dfb272c
33 changed files with 1586 additions and 571 deletions

View File

@@ -0,0 +1,136 @@
<?php
namespace App\Http\Controllers;
use App\Models\CheckoutSession;
use App\Services\Checkout\CheckoutAssignmentService;
use App\Services\Checkout\CheckoutSessionService;
use App\Services\PayPal\Exceptions\PayPalException;
use App\Services\PayPal\PayPalOrderService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class PayPalReturnController extends Controller
{
public function __construct(
private readonly PayPalOrderService $orders,
private readonly CheckoutSessionService $sessions,
private readonly CheckoutAssignmentService $assignment,
) {}
public function __invoke(Request $request): RedirectResponse
{
$orderId = $this->resolveOrderId($request);
$fallback = $this->resolveFallbackUrl();
if (! $orderId) {
return redirect()->to($fallback);
}
$session = CheckoutSession::query()
->where('paypal_order_id', $orderId)
->first();
if (! $session) {
return redirect()->to($fallback);
}
$successUrl = data_get($session->provider_metadata ?? [], 'paypal_success_url');
$cancelUrl = data_get($session->provider_metadata ?? [], 'paypal_cancel_url');
if ($session->status === CheckoutSession::STATUS_COMPLETED) {
return redirect()->to($this->resolveSafeRedirect($successUrl, $fallback));
}
try {
$capture = $this->orders->captureOrder($orderId, [
'request_id' => $session->id,
]);
} catch (PayPalException $exception) {
Log::warning('PayPal return capture failed', [
'session_id' => $session->id,
'order_id' => $orderId,
'message' => $exception->getMessage(),
'status' => $exception->status(),
]);
$this->sessions->markFailed($session, 'paypal_capture_failed');
return redirect()->to($this->resolveSafeRedirect($cancelUrl, $fallback));
}
$status = strtoupper((string) ($capture['status'] ?? ''));
$captureId = $this->orders->resolveCaptureId($capture);
$totals = $this->orders->resolveTotals($capture);
$session->forceFill([
'paypal_capture_id' => $captureId,
'provider_metadata' => array_merge($session->provider_metadata ?? [], array_filter([
'paypal_status' => $status ?: null,
'paypal_capture_id' => $captureId,
'paypal_totals' => $totals !== [] ? $totals : null,
'paypal_captured_at' => now()->toIso8601String(),
])),
])->save();
if ($status === 'COMPLETED') {
$this->sessions->markProcessing($session, [
'paypal_status' => $status,
'paypal_capture_id' => $captureId,
]);
$this->assignment->finalise($session, [
'source' => 'paypal_return',
'provider' => CheckoutSession::PROVIDER_PAYPAL,
'provider_reference' => $captureId ?? $orderId,
'payload' => $capture,
]);
$this->sessions->markCompleted($session, now());
return redirect()->to($this->resolveSafeRedirect($successUrl, $fallback));
}
$this->sessions->markFailed($session, 'paypal_'.$status);
return redirect()->to($this->resolveSafeRedirect($cancelUrl, $fallback));
}
protected function resolveOrderId(Request $request): ?string
{
$candidate = $request->query('token') ?? $request->query('order_id');
if (! is_string($candidate) || $candidate === '') {
return null;
}
return $candidate;
}
protected function resolveFallbackUrl(): string
{
return rtrim((string) config('app.url', url('/')), '/') ?: url('/');
}
protected function resolveSafeRedirect(?string $target, string $fallback): string
{
if (! $target) {
return $fallback;
}
if (Str::startsWith($target, ['/'])) {
return $target;
}
$appHost = parse_url($fallback, PHP_URL_HOST);
$targetHost = parse_url($target, PHP_URL_HOST);
if ($appHost && $targetHost && Str::lower($appHost) === Str::lower($targetHost)) {
return $target;
}
return $fallback;
}
}