WebOrbiton
v1.0.0.0

StocketBase

182 lines · 5.6 KB
  1. <?php
  2. ​
  3. declare(strict_types=1);
  4. ​
  5. require_once __DIR__ . '/../config.php';
  6. require_once __DIR__ . '/../site-front.php';
  7. ​
  8. final class StripeClient
  9. {
  10. private const API_BASE = 'https://api.stripe.com/v1';
  11. ​
  12. private string $secretKey;
  13. ​
  14. public function __construct(?string $secretKey = null)
  15. {
  16. $this->secretKey = $secretKey ?? (string) (SiteFront::settings()['payment_stripe_secret_key'] ?? '');
  17. }
  18. ​
  19. public function createCheckoutSession(
  20. array $lineItems,
  21. string $mode,
  22. string $successUrl,
  23. string $cancelUrl,
  24. ?string $customerEmail,
  25. array $metadata = [],
  26. ?string $couponId = null
  27. ): ?array {
  28. if ($this->secretKey === '') {
  29. error_log('StripeClient: cannot create checkout session, no secret key configured.');
  30. ​
  31. return null;
  32. }
  33. ​
  34. if (empty($lineItems)) {
  35. error_log('StripeClient: cannot create checkout session with no line items.');
  36. ​
  37. return null;
  38. }
  39. ​
  40. $items = [];
  41. foreach (array_values($lineItems) as $index => $item) {
  42. $priceData = [
  43. 'currency' => strtolower((string) ($item['currency'] ?? 'usd')),
  44. 'product_data' => [
  45. 'name' => (string) ($item['name'] ?? 'Item'),
  46. ],
  47. 'unit_amount' => max(0, (int) ($item['unit_amount_cents'] ?? 0)),
  48. ];
  49. ​
  50. $recurringInterval = $item['recurring_interval'] ?? null;
  51. if ($recurringInterval !== null && $recurringInterval !== '') {
  52. $priceData['recurring'] = ['interval' => (string) $recurringInterval];
  53. }
  54. ​
  55. $items[$index] = [
  56. 'price_data' => $priceData,
  57. 'quantity' => max(1, (int) ($item['quantity'] ?? 1)),
  58. ];
  59. }
  60. ​
  61. $formData = [
  62. 'mode' => $mode,
  63. 'success_url' => $successUrl,
  64. 'cancel_url' => $cancelUrl,
  65. 'line_items' => $items,
  66. ];
  67. ​
  68. if ($customerEmail !== null && $customerEmail !== '') {
  69. $formData['customer_email'] = $customerEmail;
  70. }
  71. ​
  72. foreach ($metadata as $key => $value) {
  73. $formData['metadata'][$key] = (string) $value;
  74. if ($mode === 'subscription') {
  75. $formData['subscription_data']['metadata'][$key] = (string) $value;
  76. }
  77. }
  78. ​
  79. if ($couponId !== null && $couponId !== '') {
  80. $formData['discounts'] = [['coupon' => $couponId]];
  81. }
  82. ​
  83. [$status, $body] = $this->post('/checkout/sessions', $formData);
  84. ​
  85. if ($status < 200 || $status >= 300 || !isset($body['url'])) {
  86. error_log('StripeClient: checkout session creation failed with HTTP ' . $status . ' — ' . (string) json_encode($body));
  87. ​
  88. return null;
  89. }
  90. ​
  91. return $body;
  92. }
  93. ​
  94. public function createOneTimeCoupon(int $amountOffCents, string $currency, string $name): ?string
  95. {
  96. if ($this->secretKey === '' || $amountOffCents <= 0) {
  97. return null;
  98. }
  99. ​
  100. [$status, $body] = $this->post('/coupons', [
  101. 'amount_off' => $amountOffCents,
  102. 'currency' => strtolower($currency),
  103. 'duration' => 'once',
  104. 'max_redemptions' => 1,
  105. 'name' => mb_substr($name, 0, 40),
  106. ]);
  107. ​
  108. if ($status < 200 || $status >= 300 || empty($body['id'])) {
  109. error_log('StripeClient: coupon creation failed with HTTP ' . $status . ' — ' . (string) json_encode($body));
  110. ​
  111. return null;
  112. }
  113. ​
  114. return (string) $body['id'];
  115. }
  116. ​
  117. public function createRefund(string $paymentIntentOrChargeId, ?int $amountCents = null): bool
  118. {
  119. if ($this->secretKey === '') {
  120. error_log('StripeClient: cannot create refund, no secret key configured.');
  121. ​
  122. return false;
  123. }
  124. ​
  125. $idField = str_starts_with($paymentIntentOrChargeId, 'ch_') ? 'charge' : 'payment_intent';
  126. ​
  127. $formData = [$idField => $paymentIntentOrChargeId];
  128. if ($amountCents !== null) {
  129. $formData['amount'] = max(0, $amountCents);
  130. }
  131. ​
  132. [$status, $body] = $this->post('/refunds', $formData);
  133. ​
  134. if ($status < 200 || $status >= 300) {
  135. error_log('StripeClient: refund failed with HTTP ' . $status . ' — ' . (string) json_encode($body));
  136. ​
  137. return false;
  138. }
  139. ​
  140. return true;
  141. }
  142. ​
  143. private function post(string $path, array $formData): array
  144. {
  145. $url = self::API_BASE . $path;
  146. $body = http_build_query($formData, '', '&', PHP_QUERY_RFC1738);
  147. ​
  148. if (!function_exists('curl_init')) {
  149. error_log('StripeClient: cURL extension is not available.');
  150. ​
  151. return [0, []];
  152. }
  153. ​
  154. $curl = curl_init($url);
  155. curl_setopt_array($curl, [
  156. CURLOPT_POST => true,
  157. CURLOPT_POSTFIELDS => $body,
  158. CURLOPT_HTTPHEADER => [
  159. 'Authorization: Bearer ' . $this->secretKey,
  160. 'Content-Type: application/x-www-form-urlencoded',
  161. ],
  162. CURLOPT_RETURNTRANSFER => true,
  163. CURLOPT_CONNECTTIMEOUT => 10,
  164. CURLOPT_TIMEOUT => 20,
  165. ]);
  166. $response = curl_exec($curl);
  167. $status = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
  168. $curlError = curl_error($curl);
  169. curl_close($curl);
  170. ​
  171. if ($response === false) {
  172. error_log('StripeClient: request failed — ' . $curlError);
  173. ​
  174. return [0, []];
  175. }
  176. ​
  177. $decoded = json_decode($response, true);
  178. ​
  179. return [$status, is_array($decoded) ? $decoded : []];
  180. }
  181. }
  182. ​