Add PayPal checkout provider
This commit is contained in:
161
app/Services/PayPal/PayPalOrderService.php
Normal file
161
app/Services/PayPal/PayPalOrderService.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\PayPal;
|
||||
|
||||
use App\Models\CheckoutSession;
|
||||
use App\Models\Package;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PayPalOrderService
|
||||
{
|
||||
public function __construct(private readonly PayPalClient $client) {}
|
||||
|
||||
/**
|
||||
* @param array{return_url?: string|null, cancel_url?: string|null, locale?: string|null, request_id?: string|null} $options
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function createOrder(CheckoutSession $session, Package $package, array $options = []): array
|
||||
{
|
||||
$currency = strtoupper((string) ($session->currency ?: $package->currency ?: 'EUR'));
|
||||
$total = $this->formatAmount((float) $session->amount_total);
|
||||
$subtotal = $this->formatAmount((float) $session->amount_subtotal);
|
||||
$discount = $this->formatAmount((float) $session->amount_discount);
|
||||
|
||||
$amount = [
|
||||
'currency_code' => $currency,
|
||||
'value' => $total,
|
||||
];
|
||||
|
||||
if ($subtotal !== null && $discount !== null) {
|
||||
$amount['breakdown'] = [
|
||||
'item_total' => [
|
||||
'currency_code' => $currency,
|
||||
'value' => $subtotal,
|
||||
],
|
||||
'discount' => [
|
||||
'currency_code' => $currency,
|
||||
'value' => $discount,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$purchaseUnit = [
|
||||
'reference_id' => 'package-'.$package->id,
|
||||
'description' => Str::limit($package->name ?? 'Package', 127, ''),
|
||||
'custom_id' => $session->id,
|
||||
'amount' => $amount,
|
||||
];
|
||||
|
||||
$applicationContext = array_filter([
|
||||
'brand_name' => config('app.name', 'Fotospiel'),
|
||||
'landing_page' => 'NO_PREFERENCE',
|
||||
'user_action' => 'PAY_NOW',
|
||||
'shipping_preference' => 'NO_SHIPPING',
|
||||
'locale' => $this->resolveLocale($options['locale'] ?? $session->locale),
|
||||
'return_url' => $options['return_url'] ?? null,
|
||||
'cancel_url' => $options['cancel_url'] ?? null,
|
||||
], static fn ($value) => $value !== null && $value !== '');
|
||||
|
||||
$payload = [
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => [$purchaseUnit],
|
||||
'application_context' => $applicationContext,
|
||||
];
|
||||
|
||||
$headers = [];
|
||||
$requestId = $options['request_id'] ?? $session->id;
|
||||
if (is_string($requestId) && $requestId !== '') {
|
||||
$headers['PayPal-Request-Id'] = $requestId;
|
||||
}
|
||||
|
||||
return $this->client->post('/v2/checkout/orders', $payload, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{request_id?: string|null} $options
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function captureOrder(string $orderId, array $options = []): array
|
||||
{
|
||||
$headers = [];
|
||||
$requestId = $options['request_id'] ?? null;
|
||||
if (is_string($requestId) && $requestId !== '') {
|
||||
$headers['PayPal-Request-Id'] = $requestId;
|
||||
}
|
||||
|
||||
return $this->client->post(sprintf('/v2/checkout/orders/%s/capture', $orderId), [], $headers);
|
||||
}
|
||||
|
||||
public function resolveApproveUrl(array $payload): ?string
|
||||
{
|
||||
$links = Arr::get($payload, 'links', []);
|
||||
if (! is_array($links)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($links as $link) {
|
||||
if (! is_array($link)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($link['rel'] ?? null) === 'approve') {
|
||||
return is_string($link['href'] ?? null) ? $link['href'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function resolveCaptureId(array $payload): ?string
|
||||
{
|
||||
$captureId = Arr::get($payload, 'purchase_units.0.payments.captures.0.id');
|
||||
|
||||
return is_string($captureId) && $captureId !== '' ? $captureId : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{currency?: string, total?: float}
|
||||
*/
|
||||
public function resolveTotals(array $payload): array
|
||||
{
|
||||
$amount = Arr::get($payload, 'purchase_units.0.payments.captures.0.amount')
|
||||
?? Arr::get($payload, 'purchase_units.0.amount');
|
||||
|
||||
if (! is_array($amount)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$currency = Arr::get($amount, 'currency_code');
|
||||
$total = Arr::get($amount, 'value');
|
||||
|
||||
return array_filter([
|
||||
'currency' => is_string($currency) ? strtoupper($currency) : null,
|
||||
'total' => is_numeric($total) ? (float) $total : null,
|
||||
], static fn ($value) => $value !== null);
|
||||
}
|
||||
|
||||
protected function resolveLocale(?string $locale): ?string
|
||||
{
|
||||
if (! $locale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = str_replace('_', '-', $locale);
|
||||
|
||||
return match (strtolower($normalized)) {
|
||||
'de', 'de-de', 'de-at', 'de-ch' => 'de-DE',
|
||||
'en', 'en-gb' => 'en-GB',
|
||||
default => 'en-US',
|
||||
};
|
||||
}
|
||||
|
||||
protected function formatAmount(float $amount): ?string
|
||||
{
|
||||
if (! is_finite($amount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return number_format($amount, 2, '.', '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user