6 private function config(): array {
7 $file = dirname(__DIR__) .
'/cfg/payment.php';
16 if (function_exists(
'dbx')) {
17 $shopCfg =
dbx()->get_config(
'dbxShop');
18 if (is_array($shopCfg)) {
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')),
34 $cfg = $this->config();
35 return !empty(
$cfg[
'enabled'])
36 && trim((
string)(
$cfg[
'client_id'] ??
'')) !==
''
37 && trim((
string)(
$cfg[
'client_secret'] ??
'')) !==
'';
40 public function mode(): string {
41 $cfg = $this->config();
42 return (
string)(
$cfg[
'mode'] ??
'sandbox') ===
'live' ?
'live' :
'sandbox';
46 return
'PayPal ist vorbereitet. Aktivieren Sie PayPal unter Shop > Einstellungen und tragen Sie Client-ID und Secret ein.';
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';
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);
59 $body = $payload !==
null ? json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null;
60 $httpHeaders = $headers;
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);
70 curl_setopt($ch, CURLOPT_POSTFIELDS,
$body);
73 $raw = curl_exec($ch);
74 $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
75 $err = curl_error($ch);
78 if ($raw ===
false || $err !==
'') {
79 throw new \RuntimeException(
'PayPal-Verbindung fehlgeschlagen: ' . $err);
82 $data = json_decode((
string)$raw,
true);
83 if (!is_array($data)) {
84 $data = array(
'raw' => (
string)$raw);
86 if ($status < 200 || $status >= 300) {
87 $msg = $data[
'message'] ?? $data[
'name'] ?? (
'HTTP ' . $status);
88 throw new \RuntimeException(
'PayPal-Fehler: ' . $msg);
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.');
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',
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);
117 if ($raw ===
false || $err !==
'') {
118 throw new \RuntimeException(
'PayPal-Token konnte nicht geladen werden: ' . $err);
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.');
124 return (
string)$data[
'access_token'];
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,
'.',
'');
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'] ??
''),
140 'currency_code' => $currency,
144 'payment_source' => array(
146 'experience_context' => array(
147 'brand_name' => (
string)(
$cfg[
'brand_name'] ??
'dbXapp'),
149 'shipping_preference' =>
'NO_SHIPPING',
150 'user_action' =>
'PAY_NOW',
151 'return_url' => $returnUrl,
152 'cancel_url' => $cancelUrl,
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),
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),
183 if ($paypalOrderId ===
'' || !hash_equals($paypalOrderId, (string)($capture[
'id'] ??
''))) {
184 throw new \RuntimeException(
'PayPal-Capture gehoert nicht zur erwarteten Zahlungsreferenz.');
186 if (strtoupper((
string)($capture[
'status'] ??
'')) !==
'COMPLETED') {
187 throw new \RuntimeException(
'PayPal hat die Zahlung nicht als abgeschlossen bestaetigt.');
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 =
'';
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) {
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.');
213 $capturedCurrency = $currency;
218 if (!$matchedUnit || $orderNo ===
'') {
219 throw new \RuntimeException(
'PayPal-Capture ist nicht an die lokale Bestellnummer gebunden.');
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.');
228 foreach (($paypalOrder[
'links'] ?? array()) as $link) {
229 if (($link[
'rel'] ??
'') ===
'approve' && !empty($link[
'href'])) {
230 return (
string)$link[
'href'];
240 'mode' => $this->
mode(),
241 'message' =>
'PayPal ist nicht vollstaendig konfiguriert.',
246 $token = $this->accessToken();
249 'mode' => $this->
mode(),
250 'message' =>
$token !==
''
251 ?
'PayPal OAuth Token wurde erfolgreich geladen.'
252 :
'PayPal OAuth Token ist leer.',
254 }
catch (\Throwable $e) {
257 'mode' => $this->
mode(),
258 'message' => $e->getMessage(),
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)