feat(payments): Express-Start mit Versand und Zahlartaufschlag, PATCH nur bei Abweichung

PayPal Live verlangt nach jeder Betragsaenderung eine zweite Freigabe (422
PAYER_ACTION_REQUIRED). Die Kasse rechnet den Endbetrag jetzt beim Express-Start vor
(erste aktive Versandart des Gebiets, Gast = Gebiet 1, eingeloggt = Land/Gruppe), der
Provider vergleicht vor dem PATCH mit dem Stand bei PayPal und laesst ihn bei Gleichstand
aus. Webhooks ordnen ohne custom_id ueber die PayPal-Order-ID zu.
This commit is contained in:
Thomas Bartelt
2026-09-18 07:40:27 +02:00
parent c8fc7fba2f
commit f65901060f
2 changed files with 93 additions and 14 deletions
+33 -13
View File
@@ -222,6 +222,14 @@ final class Provider extends AbstractProvider
$resource = \is_array($event['resource'] ?? null) ? $event['resource'] : [];
$orderId = (int) ($resource['custom_id'] ?? ($resource['purchase_units'][0]['custom_id'] ?? 0));
if ($orderId <= 0) {
// 18.09.2026 -- Express ohne PATCH traegt custom_id 'express': Bestellung ueber die
// PayPal-Order-ID finden (Capture: supplementary_data, Order-Events: resource.id).
$ref = (string) ($resource['supplementary_data']['related_ids']['order_id']
?? (\str_starts_with($type, 'CHECKOUT.ORDER.') ? ($resource['id'] ?? '') : ''));
$tx = $ref !== '' ? $this->transactionByReference($ref) : null;
$orderId = $tx ? (int) $tx->order_id : 0;
}
Log::write($this->module(), 'webhook', $orderId ?: null, null, (string) ($event['id'] ?? ''), $type, null, $event);
switch ($type) {
@@ -459,20 +467,32 @@ final class Provider extends AbstractProvider
$number = (string) $order->order_number;
$value = \number_format($amount, 2, '.', '');
$patch = [
['op' => 'replace', 'path' => "/purchase_units/@reference_id=='default'/amount",
'value' => ['currency_code' => $this->currency(), 'value' => $value]],
['op' => 'replace', 'path' => "/purchase_units/@reference_id=='default'/custom_id", 'value' => (string) $orderId],
['op' => 'add', 'path' => "/purchase_units/@reference_id=='default'/invoice_id", 'value' => $number . '-' . $orderId],
];
[$status, $body] = $this->client()->request('PATCH', '/v2/checkout/orders/' . \rawurlencode($reference), $patch);
Log::write($this->module(), 'express_patch', $orderId, $number, $reference, (string) $status, $status, $body ?: ['amount' => $value]);
if ($status !== 204) {
throw new \RuntimeException('PayPal Express: Betrag konnte nicht gesetzt werden (HTTP ' . $status . '): ' . \json_encode($body['details'] ?? $body));
// 18.09.2026 -- erst nachsehen, was bei PayPal steht. Stimmt der Betrag schon (die Kasse
// hat Versand und Aufschlag beim Express-Start mitgegeben), wird NICHT gepatcht: jede
// Aenderung nach der Freigabe kostet auf Live eine zweite Freigabe (422
// PAYER_ACTION_REQUIRED, Sandbox winkt das durch). custom_id bleibt dann 'express',
// die Zuordnung laeuft ueber die Referenz (payment_transactions).
[$s0, $b0] = $this->client()->request('GET', '/v2/checkout/orders/' . \rawurlencode($reference));
$ppValue = (string) ($b0['purchase_units'][0]['amount']['value'] ?? '');
$ppCur = (string) ($b0['purchase_units'][0]['amount']['currency_code'] ?? '');
if ($s0 === 200 && $ppValue === $value && $ppCur === $this->currency()) {
Log::write($this->module(), 'express_patch', $orderId, $number, $reference, 'skipped', $s0, ['amount' => $value, 'detail' => 'Betrag stimmt bereits, kein PATCH']);
$b2 = $b0;
} else {
$patch = [
['op' => 'replace', 'path' => "/purchase_units/@reference_id=='default'/amount",
'value' => ['currency_code' => $this->currency(), 'value' => $value]],
['op' => 'replace', 'path' => "/purchase_units/@reference_id=='default'/custom_id", 'value' => (string) $orderId],
['op' => 'add', 'path' => "/purchase_units/@reference_id=='default'/invoice_id", 'value' => $number . '-' . $orderId],
];
[$status, $body] = $this->client()->request('PATCH', '/v2/checkout/orders/' . \rawurlencode($reference), $patch);
Log::write($this->module(), 'express_patch', $orderId, $number, $reference, (string) $status, $status, $body ?: ['amount' => $value, 'vorher' => $ppValue]);
if ($status !== 204) {
throw new \RuntimeException('PayPal Express: Betrag konnte nicht gesetzt werden (HTTP ' . $status . '): ' . \json_encode($body['details'] ?? $body));
}
// Approve-URL fuer den Fall, dass PayPal eine erneute Freigabe verlangt
[$s2, $b2] = $this->client()->request('GET', '/v2/checkout/orders/' . \rawurlencode($reference));
}
// Approve-URL fuer den Fall, dass PayPal eine erneute Freigabe verlangt
[$s2, $b2] = $this->client()->request('GET', '/v2/checkout/orders/' . \rawurlencode($reference));
$approveUrl = '';
foreach (($b2['links'] ?? []) as $link) {
if (\in_array($link['rel'] ?? '', ['approve', 'payer-action'], true)) {