9 private const ALGORITHM =
'AMZN-PAY-RSASSA-PSS-V2';
11 private function config(): array {
12 $cfg = function_exists(
'dbx') ?
dbx()->get_config(
'dbxShop') : array();
15 'enabled' => !empty(
$cfg[
'payment_amazon_pay_enabled']),
16 'mode' => (
string)(
$cfg[
'payment_amazon_pay_mode'] ??
'sandbox') ===
'live' ?
'live' :
'sandbox',
17 'region' => in_array((
string)(
$cfg[
'payment_amazon_pay_region'] ??
'EU'), array(
'EU',
'UK',
'US',
'JP'),
true) ? (
string)
$cfg[
'payment_amazon_pay_region'] :
'EU',
18 'merchant_id' => trim((
string)(
$cfg[
'payment_amazon_pay_merchant_id'] ??
'')),
19 'store_id' => trim((
string)(
$cfg[
'payment_amazon_pay_store_id'] ??
'')),
20 'public_key_id' => trim((
string)(
$cfg[
'payment_amazon_pay_public_key_id'] ??
'')),
21 'private_key' => trim((
string)(
$cfg[
'payment_amazon_pay_private_key'] ??
'')),
22 'currency' => strtoupper(substr((
string)(
$cfg[
'default_currency'] ??
'EUR'), 0, 3)) ?:
'EUR',
23 'sandbox_simulation_code' => trim((
string)(
$cfg[
'payment_amazon_pay_sandbox_simulation_code'] ??
'')),
28 $cfg = $this->config();
29 return !empty(
$cfg[
'enabled'])
30 && (string)
$cfg[
'merchant_id'] !==
''
31 && (
string)
$cfg[
'store_id'] !==
''
32 && (string)
$cfg[
'public_key_id'] !==
''
33 && (
string)
$cfg[
'private_key'] !==
'';
36 public function mode(): string {
37 return (string)$this->config()[
'mode'];
41 $region = (string)$this->config()[
'region'];
42 if ($region ===
'US')
return 'na';
43 if ($region ===
'JP')
return 'jp';
47 private function host(): string {
48 $region = (string)$this->config()[
'region'];
49 if ($region ===
'JP')
return 'pay-api.amazon.jp';
50 if ($region ===
'EU' || $region ===
'UK')
return 'pay-api.amazon.eu';
51 return 'pay-api.amazon.com';
54 private function apiBase(): string {
55 $cfg = $this->config();
56 return 'https://' . $this->host() .
'/' . (string)
$cfg[
'mode'] .
'/v2';
59 private function normalizeHeaderValue(
string $value): string {
60 return trim(preg_replace(
'/\s+/',
' ', $value) ?: $value);
63 private function canonicalUri(
string $path): string {
64 $parts = explode(
'/', $path);
65 foreach ($parts as &$part) {
66 $part = rawurlencode(rawurldecode($part));
69 return implode(
'/', $parts);
72 private function canonicalQuery(
string $query): string {
76 parse_str($query, $params);
77 ksort($params, SORT_STRING);
79 foreach ($params as $key => $value) {
80 if (is_array($value)) {
81 sort($value, SORT_STRING);
82 foreach ($value as $item) {
83 $pairs[] = rawurlencode((
string)$key) .
'=' . rawurlencode((
string)$item);
86 $pairs[] = rawurlencode((
string)$key) .
'=' . rawurlencode((
string)$value);
89 return implode(
'&', $pairs);
92 private function authorizationHeader(
string $method,
string $path, array $headers,
string $body): string {
93 $cfg = $this->config();
94 $normalized = array();
95 foreach ($headers as $name => $value) {
96 $normalized[strtolower((
string)$name)] = $this->normalizeHeaderValue((
string)$value);
98 ksort($normalized, SORT_STRING);
100 $canonicalHeaders =
'';
101 foreach ($normalized as $name => $value) {
102 $canonicalHeaders .= $name .
':' . $value .
"\n";
104 $signedHeaders = implode(
';', array_keys($normalized));
105 $pathParts = parse_url($path);
106 $canonicalUri = $this->canonicalUri((
string)($pathParts[
'path'] ?? $path));
107 $canonicalQuery = $this->canonicalQuery((
string)($pathParts[
'query'] ??
''));
108 $canonicalRequest = strtoupper($method) .
"\n"
109 . $canonicalUri .
"\n"
110 . $canonicalQuery .
"\n"
111 . $canonicalHeaders .
"\n"
112 . $signedHeaders .
"\n"
113 . hash(
'sha256', $body);
115 $stringToSign = self::ALGORITHM .
"\n" . hash(
'sha256', $canonicalRequest);
116 $privateKey = PublicKeyLoader::loadPrivateKey((
string)
$cfg[
'private_key'])
118 ->withMGFHash(
'sha256')
120 ->withPadding(RSA::SIGNATURE_PSS);
121 $signature = base64_encode($privateKey->sign($stringToSign));
123 return self::ALGORITHM
124 .
' PublicKeyId=' . (string)
$cfg[
'public_key_id']
125 .
', SignedHeaders=' . $signedHeaders
126 .
', Signature=' . $signature;
129 private function request(
string $method,
string $path, ?array $payload =
null,
bool $idempotent =
false): array {
130 if (!$this->isConfigured()) {
131 throw new \RuntimeException(
'Amazon Pay ist nicht vollstaendig konfiguriert.');
134 $cfg = $this->config();
135 $body = $payload !==
null ? json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) :
'';
136 if (!is_string($body)) {
141 'accept' =>
'application/json',
142 'content-type' =>
'application/json',
143 'x-amz-pay-date' => gmdate(
'Ymd\THis\Z'),
144 'x-amz-pay-host' => $this->host(),
145 'x-amz-pay-region' => $this->regionCode(),
148 $headers[
'x-amz-pay-idempotency-key'] = bin2hex(random_bytes(16));
150 if ((
string)
$cfg[
'mode'] ===
'sandbox' && (
string)
$cfg[
'sandbox_simulation_code'] !==
'') {
151 $headers[
'x-amz-simulation-code'] = (string)
$cfg[
'sandbox_simulation_code'];
153 $headers[
'authorization'] = $this->authorizationHeader($method,
'/' . (
string)
$cfg[
'mode'] .
'/v2' . $path, $headers, $body);
155 $httpHeaders = array();
156 foreach ($headers as $name => $value) {
157 $httpHeaders[] = $name .
': ' . $value;
160 $ch = curl_init($this->apiBase() . $path);
161 curl_setopt($ch, CURLOPT_RETURNTRANSFER,
true);
162 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
163 curl_setopt($ch, CURLOPT_TIMEOUT, 25);
164 curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
166 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
169 $raw = curl_exec($ch);
170 $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
171 $err = curl_error($ch);
174 if ($raw ===
false || $err !==
'') {
175 throw new \RuntimeException(
'Amazon-Pay-Verbindung fehlgeschlagen: ' . $err);
178 $data = json_decode((
string)$raw,
true);
179 if (!is_array($data)) {
180 $data = array(
'raw' => (
string)$raw);
182 $data[
'_http_status'] = $status;
183 if ($status < 200 || $status >= 300) {
184 $msg = $data[
'message'] ?? $data[
'reasonCode'] ?? $data[
'reasonDescription'] ?? (
'HTTP ' . $status);
185 throw new \RuntimeException(
'Amazon-Pay-Fehler: ' . $msg);
191 $cfg = $this->config();
192 $amount = number_format((
float)($order[
'total_gross'] ?? 0), 2,
'.',
'');
193 $currency = (string)($order[
'currency'] ??
$cfg[
'currency'] ??
'EUR');
195 'webCheckoutDetails' => array(
196 'checkoutReviewReturnUrl' => $returnUrl,
197 'checkoutResultReturnUrl' => $returnUrl,
198 'checkoutCancelUrl' => $cancelUrl,
200 'storeId' => (
string)
$cfg[
'store_id'],
201 'scopes' => array(
'name',
'email',
'phoneNumber',
'billingAddress'),
202 'paymentDetails' => array(
203 'paymentIntent' =>
'AuthorizeWithCapture',
204 'canHandlePendingAuthorization' =>
false,
205 'chargeAmount' => array(
207 'currencyCode' => $currency,
210 'merchantMetadata' => array(
211 'merchantReferenceId' => (
string)($order[
'order_no'] ??
''),
212 'merchantStoreName' => (
string)(
$cfg[
'merchant_id'] ??
''),
213 'noteToBuyer' =>
'dbXapp Bestellung ' . (
string)($order[
'order_no'] ??
''),
216 return $this->request(
'POST',
'/checkoutSessions', $payload,
true);
220 $amount = number_format((float)($order[
'total_gross'] ?? 0), 2,
'.',
'');
221 $currency = (string)($order[
'currency'] ?? $this->config()[
'currency'] ??
'EUR');
222 return $this->request(
'POST',
'/checkoutSessions/' . rawurlencode($checkoutSessionId) .
'/complete', array(
223 'chargeAmount' => array(
225 'currencyCode' => $currency,
231 $details = is_array($checkoutSession[
'webCheckoutDetails'] ?? null) ? $checkoutSession[
'webCheckoutDetails'] : array();
232 foreach (array(
'amazonPayRedirectUrl',
'checkoutUrl') as $key) {
233 if (!empty($details[$key])) {
234 return (
string)$details[$key];
244 'mode' => $this->
mode(),
246 'message' =>
'Amazon Pay ist nicht vollstaendig konfiguriert.',
251 $this->authorizationHeader(
'GET',
'/' . $this->mode() .
'/v2/checkoutSessions/test', array(
252 'accept' =>
'application/json',
253 'content-type' =>
'application/json',
254 'x-amz-pay-date' => gmdate(
'Ymd\THis\Z'),
255 'x-amz-pay-host' => $this->host(),
256 'x-amz-pay-region' => $this->regionCode(),
260 'mode' => $this->mode(),
261 'region' => $this->regionCode(),
262 'message' =>
'Amazon-Pay-Konfiguration und RSA-PSS-Signatur sind lokal gueltig. Der Live-API-Test erfolgt bei einer echten Checkout Session.',
264 }
catch (\Throwable $e) {
267 'mode' => $this->mode(),
268 'region' => $this->regionCode(),
269 'message' => $e->getMessage(),