dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxShopPayPal.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxShop;
3
5
6 private function config(): array {
7 $file = dirname(__DIR__) . '/cfg/payment.php';
8 $paypal = array();
9 if (!is_file($file)) {
10 $paypal = array();
11 } else {
12 $cfg = include $file;
13 $paypal = is_array($cfg) ? ($cfg['paypal'] ?? array()) : array();
14 }
15
16 if (function_exists('dbx')) {
17 $shopCfg = dbx()->get_config('dbxShop');
18 if (is_array($shopCfg)) {
19 $paypal = array_merge($paypal, array(
20 'enabled' => !empty($shopCfg['payment_paypal_enabled']),
21 'mode' => (string)($shopCfg['payment_paypal_mode'] ?? ($paypal['mode'] ?? 'sandbox')),
22 'client_id' => (string)($shopCfg['payment_paypal_client_id'] ?? ($paypal['client_id'] ?? '')),
23 'client_secret' => (string)($shopCfg['payment_paypal_client_secret'] ?? ($paypal['client_secret'] ?? '')),
24 'brand_name' => (string)($shopCfg['payment_paypal_brand_name'] ?? ($paypal['brand_name'] ?? 'dbXapp')),
25 'currency' => (string)($shopCfg['payment_paypal_currency'] ?? $shopCfg['default_currency'] ?? ($paypal['currency'] ?? 'EUR')),
26 ));
27 }
28 }
29
30 return $paypal;
31 }
32
33 public function isConfigured(): bool {
34 $cfg = $this->config();
35 return !empty($cfg['enabled'])
36 && trim((string)($cfg['client_id'] ?? '')) !== ''
37 && trim((string)($cfg['client_secret'] ?? '')) !== '';
38 }
39
40 public function mode(): string {
41 $cfg = $this->config();
42 return (string)($cfg['mode'] ?? 'sandbox') === 'live' ? 'live' : 'sandbox';
43 }
44
45 public function configHint(): string {
46 return 'PayPal ist vorbereitet. Aktivieren Sie PayPal unter Shop > Einstellungen und tragen Sie Client-ID und Secret ein.';
47 }
48
49 private function apiBase(): string {
50 $cfg = $this->config();
51 return (string)($cfg['mode'] ?? 'sandbox') === 'live'
52 ? 'https://api-m.paypal.com'
53 : 'https://api-m.sandbox.paypal.com';
54 }
55
56 private function request(string $method, string $path, array $headers = array(), ?array $payload = null, ?string $basicUser = null, ?string $basicPassword = null): array {
57 $ch = curl_init($this->apiBase() . $path);
58 $method = strtoupper($method);
59 $body = $payload !== null ? json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null;
60 $httpHeaders = $headers;
61
62 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
63 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
64 curl_setopt($ch, CURLOPT_TIMEOUT, 25);
65 curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
66 if ($basicUser !== null) {
67 curl_setopt($ch, CURLOPT_USERPWD, $basicUser . ':' . (string)$basicPassword);
68 }
69 if ($body !== null) {
70 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
71 }
72
73 $raw = curl_exec($ch);
74 $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
75 $err = curl_error($ch);
76 curl_close($ch);
77
78 if ($raw === false || $err !== '') {
79 throw new \RuntimeException('PayPal-Verbindung fehlgeschlagen: ' . $err);
80 }
81
82 $data = json_decode((string)$raw, true);
83 if (!is_array($data)) {
84 $data = array('raw' => (string)$raw);
85 }
86 if ($status < 200 || $status >= 300) {
87 $msg = $data['message'] ?? $data['name'] ?? ('HTTP ' . $status);
88 throw new \RuntimeException('PayPal-Fehler: ' . $msg);
89 }
90 return $data;
91 }
92
93 private function accessToken(): string {
94 $cfg = $this->config();
95 $clientId = trim((string)($cfg['client_id'] ?? ''));
96 $secret = trim((string)($cfg['client_secret'] ?? ''));
97 if ($clientId === '' || $secret === '') {
98 throw new \RuntimeException('PayPal-Zugangsdaten fehlen.');
99 }
100
101 $ch = curl_init($this->apiBase() . '/v1/oauth2/token');
102 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
103 curl_setopt($ch, CURLOPT_POST, true);
104 curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
105 curl_setopt($ch, CURLOPT_USERPWD, $clientId . ':' . $secret);
106 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
107 'Accept: application/json',
108 'Accept-Language: de_DE',
109 'Content-Type: application/x-www-form-urlencoded',
110 ));
111 curl_setopt($ch, CURLOPT_TIMEOUT, 25);
112 $raw = curl_exec($ch);
113 $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
114 $err = curl_error($ch);
115 curl_close($ch);
116
117 if ($raw === false || $err !== '') {
118 throw new \RuntimeException('PayPal-Token konnte nicht geladen werden: ' . $err);
119 }
120 $data = json_decode((string)$raw, true);
121 if ($status < 200 || $status >= 300 || !is_array($data) || empty($data['access_token'])) {
122 throw new \RuntimeException('PayPal-Token wurde abgelehnt.');
123 }
124 return (string)$data['access_token'];
125 }
126
127 public function createOrder(array $order, string $returnUrl, string $cancelUrl): array {
128 $cfg = $this->config();
129 $token = $this->accessToken();
130 $currency = (string)($cfg['currency'] ?? $order['currency'] ?? 'EUR');
131 $amount = number_format((float)($order['total_gross'] ?? 0), 2, '.', '');
132
133 $payload = array(
134 'intent' => 'CAPTURE',
135 'purchase_units' => array(array(
136 'reference_id' => (string)($order['order_no'] ?? ''),
137 'description' => 'dbXapp Bestellung ' . (string)($order['order_no'] ?? ''),
138 'invoice_id' => (string)($order['order_no'] ?? ''),
139 'amount' => array(
140 'currency_code' => $currency,
141 'value' => $amount,
142 ),
143 )),
144 'payment_source' => array(
145 'paypal' => array(
146 'experience_context' => array(
147 'brand_name' => (string)($cfg['brand_name'] ?? 'dbXapp'),
148 'locale' => 'de-DE',
149 'shipping_preference' => 'NO_SHIPPING',
150 'user_action' => 'PAY_NOW',
151 'return_url' => $returnUrl,
152 'cancel_url' => $cancelUrl,
153 ),
154 ),
155 ),
156 );
157
158 return $this->request('POST', '/v2/checkout/orders', array(
159 'Content-Type: application/json',
160 'Authorization: Bearer ' . $token,
161 'Prefer: return=representation',
162 'PayPal-Request-Id: dbx-' . substr(hash('sha256', 'create|' . (string)($order['order_no'] ?? '')), 0, 32),
163 ), $payload);
164 }
165
166 public function capture(string $paypalOrderId): array {
167 $token = $this->accessToken();
168 return $this->request('POST', '/v2/checkout/orders/' . rawurlencode($paypalOrderId) . '/capture', array(
169 'Content-Type: application/json',
170 'Authorization: Bearer ' . $token,
171 'Prefer: return=representation',
172 'PayPal-Request-Id: dbx-' . substr(hash('sha256', 'capture|' . $paypalOrderId), 0, 32),
173 ));
174 }
175
182 public function validateCapture(array $capture, array $order, string $paypalOrderId): void {
183 if ($paypalOrderId === '' || !hash_equals($paypalOrderId, (string)($capture['id'] ?? ''))) {
184 throw new \RuntimeException('PayPal-Capture gehoert nicht zur erwarteten Zahlungsreferenz.');
185 }
186 if (strtoupper((string)($capture['status'] ?? '')) !== 'COMPLETED') {
187 throw new \RuntimeException('PayPal hat die Zahlung nicht als abgeschlossen bestaetigt.');
188 }
189
190 $orderNo = (string)($order['order_no'] ?? '');
191 $expectedCurrency = strtoupper((string)($order['currency'] ?? 'EUR'));
192 $expectedAmount = round((float)($order['total_gross'] ?? 0), 2);
193 $matchedUnit = false;
194 $capturedAmount = 0.0;
195 $capturedCurrency = '';
196
197 foreach ((array)($capture['purchase_units'] ?? array()) as $unit) {
198 if (!is_array($unit)) continue;
199 $captures = (array)($unit['payments']['captures'] ?? array());
200 $unitMatches = (string)($unit['reference_id'] ?? '') === $orderNo
201 || (string)($unit['invoice_id'] ?? '') === $orderNo;
202 foreach ($captures as $item) {
203 if (!is_array($item) || strtoupper((string)($item['status'] ?? '')) !== 'COMPLETED') continue;
204 if ((string)($item['invoice_id'] ?? '') === $orderNo) {
205 $unitMatches = true;
206 }
207 if (!$unitMatches) continue;
208 $capturedAmount += (float)($item['amount']['value'] ?? 0);
209 $currency = strtoupper((string)($item['amount']['currency_code'] ?? ''));
210 if ($capturedCurrency !== '' && $currency !== $capturedCurrency) {
211 throw new \RuntimeException('PayPal-Capture enthaelt unterschiedliche Waehrungen.');
212 }
213 $capturedCurrency = $currency;
214 $matchedUnit = true;
215 }
216 }
217
218 if (!$matchedUnit || $orderNo === '') {
219 throw new \RuntimeException('PayPal-Capture ist nicht an die lokale Bestellnummer gebunden.');
220 }
221 if ($capturedCurrency !== $expectedCurrency
222 || abs(round($capturedAmount, 2) - $expectedAmount) > 0.001) {
223 throw new \RuntimeException('PayPal-Capture-Betrag oder Waehrung stimmt nicht mit der Bestellung ueberein.');
224 }
225 }
226
227 public function approvalUrl(array $paypalOrder): string {
228 foreach (($paypalOrder['links'] ?? array()) as $link) {
229 if (($link['rel'] ?? '') === 'approve' && !empty($link['href'])) {
230 return (string)$link['href'];
231 }
232 }
233 return '';
234 }
235
236 public function testConnection(): array {
237 if (!$this->isConfigured()) {
238 return array(
239 'ok' => false,
240 'mode' => $this->mode(),
241 'message' => 'PayPal ist nicht vollstaendig konfiguriert.',
242 );
243 }
244
245 try {
246 $token = $this->accessToken();
247 return array(
248 'ok' => $token !== '',
249 'mode' => $this->mode(),
250 'message' => $token !== ''
251 ? 'PayPal OAuth Token wurde erfolgreich geladen.'
252 : 'PayPal OAuth Token ist leer.',
253 );
254 } catch (\Throwable $e) {
255 return array(
256 'ok' => false,
257 'mode' => $this->mode(),
258 'message' => $e->getMessage(),
259 );
260 }
261 }
262}
263?>
$cfg
createOrder(array $order, string $returnUrl, string $cancelUrl)
capture(string $paypalOrderId)
validateCapture(array $capture, array $order, string $paypalOrderId)
Verifiziert die serverseitige Capture-Antwort gegen die lokale Bestellung.
if(preg_match('/core\.js\? foreach[^"\']*v=(\d+)/', $designTemplate, $assetMatch) !== 1 || !str_contains($shopReference, 'dbxapp-Asset-Version ' . $assetMatch[1])) foreach (array( 'reference\\archive', 'provision_docs_content.php', 'dbxSelfTest') as $needle)(array('Installation'=> $installation, 'Installations-Tutorial'=> $installationTutorial, 'SelfTest'=> $selfTest) as $label=> $html)
$dbxMailDeliveryTestKernel mode
if(!defined( 'IMG_WEBP')) define( 'IMG_WEBP'
DBX schema administration.