Files
shop-old/core/paypal_v2/Orders.php
T

153 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace PaypalV2;
/**
* Kapselt die Orders-v2-Endpunkte: create / get / capture.
*/
final class Orders
{
private PayPalClient $client;
public function __construct(?PayPalClient $client = null)
{
$this->client = $client ?? new PayPalClient();
}
/**
* Erstellt eine PayPal-Order (intent=CAPTURE) und liefert Order-ID + Approve-URL.
*
* @return array{id:string,approve_url:string}
*/
public function create(
float $amount,
string $orderNumber,
string $shopOrderId,
string $returnUrl,
string $cancelUrl
): array {
$value = \number_format($amount, 2, '.', '');
$currency = (string) Config::get('currency', 'EUR');
$payload = [
'intent' => 'CAPTURE',
'purchase_units' => [[
'amount' => [
'currency_code' => $currency,
'value' => $value,
],
'custom_id' => $shopOrderId,
'invoice_id' => $orderNumber . '-' . $shopOrderId,
]],
'application_context' => [
'brand_name' => (string) Config::get('brand_name', 'Shop'),
'locale' => 'de-DE',
'landing_page' => 'NO_PREFERENCE',
'shipping_preference' => 'NO_SHIPPING',
'user_action' => 'PAY_NOW',
'return_url' => $returnUrl,
'cancel_url' => $cancelUrl,
],
];
// Idempotenz: gleicher Request-Id => PayPal legt keine Doppel-Order an.
$requestId = 'create-' . $shopOrderId . '-' . \substr(\hash('sha256', $orderNumber . '|' . $value), 0, 16);
[$status, $body] = $this->client->request('POST', '/v2/checkout/orders', $payload, [
'PayPal-Request-Id: ' . $requestId,
]);
if ($status !== 201 || empty($body['id'])) {
throw new \RuntimeException('PayPal v2: Order-Anlage fehlgeschlagen (HTTP ' . $status . '): ' . \json_encode($body));
}
$approve = '';
foreach (($body['links'] ?? []) as $link) {
$rel = $link['rel'] ?? '';
if ($rel === 'approve' || $rel === 'payer-action') {
$approve = (string) ($link['href'] ?? '');
break;
}
}
if ($approve === '') {
throw new \RuntimeException('PayPal v2: Kein Approve-Link in der Antwort.');
}
return ['id' => (string) $body['id'], 'approve_url' => $approve];
}
/** @return array<mixed> */
public function get(string $paypalOrderId): array
{
[$status, $body] = $this->client->request('GET', '/v2/checkout/orders/' . \rawurlencode($paypalOrderId));
if ($status !== 200) {
throw new \RuntimeException('PayPal v2: Order-Abruf fehlgeschlagen (HTTP ' . $status . ').');
}
return $body;
}
/**
* Captured eine vom Kunden freigegebene Order. Idempotent:
* Ist bereits gecaptured (Doppel-Return / Webhook-Race), wird der bestehende
* Zustand aus der Order gelesen statt einen Fehler zu werfen.
*
* @return array{order_status:string,capture_id:string,capture_status:string,amount:string,currency:string,payer_email:string,payer_id:string,raw:array}
*/
public function capture(string $paypalOrderId): array
{
[$status, $body] = $this->client->request(
'POST',
'/v2/checkout/orders/' . \rawurlencode($paypalOrderId) . '/capture',
null,
[
'PayPal-Request-Id: capture-' . $paypalOrderId,
'Prefer: return=representation',
]
);
if ($status === 201 || $status === 200) {
return $this->normalize($body);
}
if ($status === 422 && $this->hasIssue($body, 'ORDER_ALREADY_CAPTURED')) {
return $this->normalize($this->get($paypalOrderId));
}
throw new \RuntimeException('PayPal v2: Capture fehlgeschlagen (HTTP ' . $status . '): ' . \json_encode($body));
}
/**
* Vereinheitlicht Capture- bzw. Order-Antwort auf die Felder, die der Shop braucht.
*
* @param array<mixed> $body
* @return array{order_status:string,capture_id:string,capture_status:string,amount:string,currency:string,payer_email:string,payer_id:string,raw:array}
*/
public function normalize(array $body): array
{
$cap = $body['purchase_units'][0]['payments']['captures'][0] ?? [];
return [
'order_status' => (string) ($body['status'] ?? ''),
'capture_id' => (string) ($cap['id'] ?? ''),
'capture_status' => (string) ($cap['status'] ?? ''),
'amount' => (string) ($cap['amount']['value'] ?? ''),
'currency' => (string) ($cap['amount']['currency_code'] ?? ''),
'payer_email' => (string) ($body['payer']['email_address'] ?? ''),
'payer_id' => (string) ($body['payer']['payer_id'] ?? ''),
'raw' => $body,
];
}
/** @param array<mixed> $body */
private function hasIssue(array $body, string $issue): bool
{
foreach (($body['details'] ?? []) as $d) {
if (($d['issue'] ?? '') === $issue) {
return true;
}
}
return false;
}
}