PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.3
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / square / class-charitable-square-api.php

class-charitable-square-api.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.12.3, at includes/square/class-charitable-square-api.php

2,338 lines 58.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Super-simple, minimum abstraction Square API wrapper.
4 *
5 * @package Charitable Square/Classes
6 * @author WP Charitable
7 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
8 * @since 1.8.7
9 * @version 1.8.7
10 */
11
12 use Square\Legacy\Exceptions\ApiException;
13 use Square\Types\CreateOrderRequest;
14 use Square\Types\Order;
15 use Square\Types\OrderSource;
16 use Square\Types\OrderLineItem;
17 use Square\Types\Money;
18 use Square\Exceptions\SquareApiException;
19 use Square\Payments\Requests\CreatePaymentRequest;
20 use Square\ApiResponse;
21 use Square\Types\Address;
22 use Square\Types\Payment;
23 use Square\Types\OrderLineItemDiscount;
24 use Square\Types\OrderState;
25 use Square\Refunds\Requests\RefundPaymentRequest;
26 use Square\Customers\Requests\CreateCustomerRequest;
27 use Square\Cards\Requests\CreateCardRequest;
28 use Square\Types\Card;
29 use Square\Types\CatalogQuery;
30 use Square\Types\CatalogQueryExact;
31 use Square\Types\CatalogObject;
32 use Square\Types\CatalogObjectType;
33 use Square\Types\Customer;
34 use Square\Types\SubscriptionSource;
35 use Square\Types\SubscriptionPricing;
36 use Square\Types\SubscriptionPhase;
37 use Square\Types\CatalogSubscriptionPlan;
38 use Square\Types\CatalogSubscriptionPlanVariation;
39 use Square\Types\CatalogObjectSubscriptionPlan;
40 use Square\Types\CatalogObjectSubscriptionPlanVariation;
41 use Square\Orders\Requests\UpdateOrderRequest;
42 use Square\Catalog\Requests\SearchCatalogObjectsRequest;
43 use Square\Catalog\Object\Requests\UpsertCatalogObjectRequest;
44 use Square\Subscriptions\Requests\CreateSubscriptionRequest;
45 use Square\Legacy\Models\Builders\CatalogObjectBuilder;
46 use Square\Subscriptions\Requests\CancelSubscriptionsRequest;
47
48
49 use Square\Customers\CustomersClient;
50 use Square\Core\Client\RawClient;
51 use Square\Cards\CardsClient;
52 use Square\Catalog\CatalogClient;
53
54 if ( ! defined( 'ABSPATH' ) ) {
55 exit;
56 }
57
58 /**
59 * API class.
60 *
61 * @since 1.8.7
62 */
63 class Charitable_Square_API {
64
65 /**
66 * The Square API version to target
67 *
68 * @since 1.8.7
69 *
70 * @var string
71 */
72 const SQUARE_VERSION = '2025-03-19';
73
74 /**
75 * Square application name.
76 *
77 * @since 1.8.7
78 */
79 public const APP_NAME = 'WPCharitable';
80
81 /**
82 * Square API client instance.
83 *
84 * @since 1.8.7
85 *
86 * @var SquareClient
87 */
88 private $client;
89
90 /**
91 * Payment token (card nonce) generated by the Web Payments SDK.
92 *
93 * @since 1.8.7
94 *
95 * @var string
96 */
97 private $source_id;
98
99 /**
100 * Last API call response.
101 *
102 * @since 1.8.7
103 *
104 * @var ApiResponse
105 */
106 private $response;
107
108 /**
109 * Last API call exception.
110 *
111 * @since 1.8.7
112 *
113 * @var ApiException
114 */
115 private $exception;
116
117 /**
118 * The base URL that we will call with our API requests.
119 *
120 * @since 1.8.7
121 *
122 * @var string
123 */
124 private $api_base_url;
125
126 /**
127 * The Live Access Token supplied by the user.
128 *
129 * @since 1.8.7
130 *
131 * @var string
132 */
133 private $live_access_token;
134
135 /**
136 * The Test Access Token supplied by the user.
137 *
138 * @since 1.8.7
139 *
140 * @var string
141 */
142 private $test_access_token;
143
144 /**
145 * The Access Token supplied by the user.
146 *
147 * @since 1.8.7
148 *
149 * @var string
150 */
151 private $access_token;
152
153 /**
154 * The Refresh Token supplied by the user.
155 *
156 * @since 1.8.7
157 *
158 * @var string
159 */
160 private $refresh_token;
161
162 /**
163 * The Client ID supplied by the user.
164 *
165 * @since 1.8.7
166 *
167 * @var string
168 */
169 private $merchant_id;
170
171 /**
172 *
173 * The ID of the business location to associate the payment with
174 *
175 * @since 1.8.7
176 *
177 * @var string
178 */
179 private $location_id;
180
181 /**
182 * Whether the Access Token was validated.
183 *
184 * @since 1.8.7
185 *
186 * @var boolean
187 */
188 private $access_token_validated;
189
190 /**
191 * The response to the most recent request.
192 *
193 * @since 1.8.7
194 *
195 * @var WP_Error|?
196 */
197 private $last_response;
198
199 /**
200 * Whether the test mode is enabled.
201 *
202 * @since 1.8.7
203 *
204 * @var boolean
205 */
206 private $test_mode;
207
208 /**
209 * The Client ID supplied by the user.
210 *
211 * @since 1.8.7
212 *
213 * @var string
214 */
215 private $client_id;
216
217 /**
218 * API errors.
219 *
220 * @since 1.8.7
221 *
222 * @var array
223 */
224 private $errors;
225
226 /**
227 * Maximum number of retries for rate-limited requests.
228 *
229 * @since 1.8.7
230 *
231 * @var int
232 */
233 const MAX_RETRIES = 5;
234
235 /**
236 * Base delay in seconds for exponential backoff.
237 *
238 * @since 1.8.7
239 *
240 * @var int
241 */
242 const BASE_DELAY = 1;
243
244 /**
245 * Create a new instance.
246 *
247 * @since 1.8.7
248 *
249 * @param Charitable_Square_Connection $connection The connection object.
250 *
251 * Connection object structure:
252 * - live_mode (private): null
253 * - access_token (private): string - OAuth access token
254 * - refresh_token (private): string - OAuth refresh token
255 * - client_id (private): string - Square application client ID
256 * - merchant_id (private): string - Square merchant ID
257 * - currency (private): null
258 * - status (private): string - Connection status ("valid")
259 * - renew_at (private): int - Unix timestamp for token renewal
260 * - encrypted (private): bool - Whether tokens are encrypted
261 * - scopes_updated (private): int - Unix timestamp of last scope update
262 */
263
264
265 /**
266 * Constructor.
267 *
268 * @since 1.8.7
269 *
270 * @param Charitable_Square_Connection $connection The connection object.
271 */
272 public function __construct( $connection ) {
273
274 if ( empty( $connection ) ) {
275 return;
276 }
277
278 $this->client = new \Square\SquareClient(
279 token: $connection->get_access_token(),
280 options: array(
281 'baseUrl' => $connection->get_mode() !== 'live' ? \Square\Environments::Sandbox->value : \Square\Environments::Production->value,
282 ),
283 );
284
285 // Set the location ID.
286 $this->location_id = charitable_square_get_location_id();
287
288 if ( empty( $this->location_id ) ) {
289 if ( charitable_is_debug( 'square' ) ) {
290 // phpcs:disable
291 error_log( 'Square API: No location ID found. Please configure your Square location ID in the plugin settings.' );
292 // phpcs:enable
293 }
294 return;
295 }
296
297 $this->test_mode = $connection->get_mode();
298 $this->api_base_url = $this->test_mode ? 'https://connect.squareupsandbox.com/v2' : 'https://connect.squareup.com/v2';
299 $this->access_token = $connection->get_access_token();
300 $this->refresh_token = $connection->get_refresh_token();
301 $this->client_id = $connection->get_client_id();
302 $this->merchant_id = $connection->get_merchant_id();
303 }
304
305 /**
306 * Set tokens from a submitted form data.
307 *
308 * @since 1.8.7
309 *
310 * @param string $square_token The Square token.
311 */
312 public function set_payment_tokens( string $square_token ) {
313
314 if ( ! empty( $square_token ) ) {
315 $this->source_id = $square_token;
316 }
317 }
318
319 /**
320 * Set up hooks.
321 *
322 * @since 1.8.7
323 *
324 * @return void
325 */
326 public function init() {
327 // Nothing much here yet. Add any filters pertaining to API here etc..
328 }
329
330 /**
331 * Check if OAuth connection is present and ready to use.
332 *
333 * @since 1.8.7
334 */
335 private function check_connection() {
336
337 $connection = Charitable_Square_Connection::get();
338
339 if ( ! $connection || ! $connection->is_configured() ) {
340 $this->errors[] = esc_html__( 'Square account connection is missing.', 'charitable' );
341
342 return;
343 }
344
345 if ( ! $connection->is_valid() ) {
346 $this->errors[] = esc_html__( 'Square account connection is invalid.', 'charitable' );
347
348 return;
349 }
350
351 if ( ! $connection->is_currency_matched() ) {
352 $this->errors[] = esc_html__( 'The currency associated with the payment is not valid for the provided business location.', 'charitable' );
353 }
354 }
355
356 /**
357 * Check if single payment tokens are present.
358 *
359 * @since 1.8.7
360 */
361 private function check_payment_tokens() {
362
363 if ( empty( $this->source_id ) ) {
364 $this->errors[] = esc_html__( 'Square payment stopped, missing card tokens.', 'charitable' );
365 }
366 }
367
368 /**
369 * Check if all required general arguments are present.
370 *
371 * @since 1.8.7
372 *
373 * @param array $args Arguments to check.
374 */
375 private function check_required_args_general( array $args ) {
376
377 if ( empty( $args['location_id'] ) ) {
378 $this->errors[] = esc_html__( 'Missing location ID.', 'charitable' );
379 }
380
381 if ( empty( $args['currency'] ) ) {
382 $this->errors[] = esc_html__( 'Missing currency.', 'charitable' );
383 }
384
385 if ( empty( $args['amount'] ) && ! is_numeric( $args['amount'] ) ) {
386 $this->errors[] = esc_html__( 'Missing amount.', 'charitable' );
387 }
388 }
389
390 /**
391 * Check if all required single payment arguments are present.
392 *
393 * @since 1.8.7
394 *
395 * @param array $args Arguments to check.
396 */
397 private function check_required_args_single( array $args ) {
398
399 if ( empty( $args['order_items'] ) ) {
400 $this->errors[] = esc_html__( 'Missing order/payment items.', 'charitable' );
401 }
402 }
403
404 /**
405 * Process single transaction.
406 *
407 * @since 1.8.7
408 *
409 * @param array $args Payment arguments.
410 */
411 public function process_single_transaction( array $args ) {
412
413 $this->check_connection();
414 $this->check_payment_tokens();
415 $this->check_required_args_general( $args );
416 $this->check_required_args_single( $args );
417
418 if ( $this->errors ) {
419
420 if ( charitable_is_debug( 'square' ) ) {
421 // phpcs:disable
422 error_log( 'Charitable_Square_API. process_single_transaction - before perform_single_transaction errors' );
423 error_log( print_r( $this->errors, true ) );
424 // phpcs:enable
425 }
426
427 return $this->errors;
428 }
429
430 $result = $this->perform_single_transaction( $args );
431
432 if ( $this->errors ) {
433
434 if ( charitable_is_debug( 'square' ) ) {
435 // phpcs:disable
436 error_log( 'Charitable_Square_API. process_single_transaction - after perform_single_transaction errors' );
437 error_log( print_r( $this->errors, true ) );
438 // phpcs:enable
439 }
440
441 return $this->errors;
442 }
443
444 if ( charitable_is_debug( 'square' ) ) {
445 // phpcs:disable
446 error_log( 'process_single_transaction - result' );
447 error_log( print_r( $result, true ) );
448 // phpcs:enable
449 }
450
451 /**
452 * Fire when a single transaction is performed.
453 *
454 * @since 1.8.7
455 *
456 * @param array $result Single transaction result.
457 * @param array $args Payment arguments.
458 * @param Api $api Api class instance.
459 */
460 do_action( 'charitable_square_api_process_single_transaction_after', $result, $args, $this );
461
462 return $result;
463 }
464
465 /**
466 * Process subscription transaction.
467 *
468 * @since 1.8.7
469 *
470 * @param array $args Payment arguments.
471 */
472 public function process_subscription_transaction( array $args ) {
473
474 $this->check_connection();
475 $this->check_payment_tokens();
476 $this->check_required_args_general( $args );
477 $this->check_required_args_subscription( $args );
478
479 if ( $this->errors ) {
480 return;
481 }
482
483 $result = $this->perform_subscription_transaction( $args );
484
485 return $result;
486 }
487
488 /**
489 * Perform a single transaction.
490 *
491 * @since 1.8.7
492 *
493 * @param array $args Payment arguments.
494 *
495 * @return array
496 */
497 private function perform_single_transaction( array $args ): array {
498
499 $result = array();
500 // Create an order.
501 $order_request = $this->prepare_create_order_request( $args );
502
503 if ( charitable_is_debug( 'square' ) ) {
504 // phpcs:disable
505 error_log( 'Charitable_Square_API. perform_single_transaction - order_request' );
506 error_log( print_r( $order_request, true ) );
507 // phpcs:enable
508 }
509
510 $order = $this->send_create_order_request( $order_request );
511
512 if ( charitable_is_debug( 'square' ) ) {
513 // phpcs:disable
514 error_log( 'Charitable_Square_API. perform_single_transaction - order' );
515 error_log( print_r( $order, true ) );
516 // phpcs:enable
517 }
518
519 if ( ! $order instanceof Order ) {
520 error_log( 'Charitable_Square_API. perform_single_transaction - order not created' ); // phpcs:ignore
521 return $result;
522 }
523
524 $result['order'] = $order;
525
526 // Create a payment.
527 $payment_request = $this->prepare_create_payment_request( $order, $args );
528 $payment = $this->send_create_payment_request( $payment_request );
529
530 if ( charitable_is_debug( 'square' ) ) {
531 // phpcs:disable
532 error_log( 'Charitable_Square_API. perform_single_transaction - payment' );
533 error_log( print_r( $payment, true ) );
534 // phpcs:enable
535 }
536
537 // In this case we should cancel an order.
538 if ( ! $payment instanceof Payment ) {
539 $update_order_request = $this->prepare_update_order_request( $order );
540 $updated_order = $this->send_update_order_request( $order->getId(), $update_order_request );
541
542 return $updated_order instanceof Order ? array( 'order' => $updated_order ) : $result;
543 }
544
545 $result['payment'] = $payment;
546
547 return $result;
548 }
549
550 /**
551 * Prepare a create order request object for sending to API.
552 *
553 * @since 1.8.7
554 *
555 * @param array $args Single payment arguments.
556 *
557 * @return CreateOrderRequest
558 */
559 private function prepare_create_order_request( array $args ) {
560
561 $request = $this->get_create_order_request_object( $args );
562 $request = $this->create_order_request_set_line_items( $request, $args );
563
564 /**
565 * Filter a create order request object.
566 *
567 * @since 1.8.7
568 *
569 * @param CreateOrderRequest $request Create order request object.
570 * @param array $args Payment arguments.
571 * @param Api $api Api class instance.
572 */
573 return apply_filters( 'charitable_square_api_prepare_create_order_request', $request, $args, $this );
574 }
575
576 /**
577 * Retrieve a create order request object.
578 *
579 * @since 1.8.7
580 *
581 * @param array $args Payment arguments.
582 *
583 * @return CreateOrderRequest
584 */
585 private function get_create_order_request_object( array $args ) {
586
587 $request = new CreateOrderRequest();
588
589 $request->setIdempotencyKey( $this->get_idempotency_key() );
590
591 $request->setOrder( new Order( array( 'locationId' => $args['location_id'] ) ) );
592 $request->getOrder()->setSource( new OrderSource( array( 'name' => self::APP_NAME ) ) );
593
594 return $request;
595 }
596
597 /**
598 * Retrieve an update order request object.
599 *
600 * @since 1.8.7
601 *
602 * @param Order $order Order object.
603 *
604 * @return UpdateOrderRequest
605 */
606 private function get_update_order_request_object( $order ): UpdateOrderRequest {
607
608 $request = new UpdateOrderRequest(
609 array(
610 'orderId' => $order->getId(),
611 'order' => $order,
612 'idempotencyKey' => $this->get_idempotency_key(),
613 )
614 );
615
616 $request->getOrder()->setState( \Square\Types\OrderState::Canceled->value );
617
618 return $request;
619 }
620
621 /**
622 * Set line items to a create order request object.
623 *
624 * @since 1.8.7
625 *
626 * @param CreateOrderRequest $request Request to create an order.
627 * @param array $args Payment arguments.
628 *
629 * @return CreateOrderRequest
630 */
631 private function create_order_request_set_line_items( $request, array $args ): CreateOrderRequest {
632
633 $line_items = array();
634
635 foreach ( (array) $args['order_items'] as $item ) {
636
637 // Order item without variations.
638 if ( empty( $item['variations'] ) ) {
639 $line_items[] = $this->get_order_line_item_object( $item, $args );
640
641 continue;
642 }
643
644 // Order item with variations (e.g. Small, Medium and Large).
645 foreach ( (array) $item['variations'] as $variation ) {
646
647 $order_line_item = $this->get_order_line_item_object( $variation, $args );
648
649 $order_line_item->setVariationName( $variation['variation_name'] );
650
651 $line_items[] = $order_line_item;
652 }
653 }
654
655 $request->getOrder()->setLineItems( $line_items );
656
657 return $request;
658 }
659
660 /**
661 * Retrieve a single order line item object.
662 *
663 * @since 1.8.7
664 *
665 * @param array $item Order item data.
666 * @param array $args Payment arguments.
667 *
668 * @return OrderLineItem
669 */
670 private function get_order_line_item_object( array $item, array $args ) {
671
672 $base_price_money = new Money();
673 $base_price_money->setAmount( round( $item['amount'] ) );
674 $base_price_money->setCurrency( $args['currency'] );
675
676 $order_line_items = new OrderLineItem(
677 array(
678 'quantity' => $item['quantity'],
679 'name' => $item['name'],
680 'basePriceMoney' => $base_price_money,
681 )
682 );
683
684 return $order_line_items;
685 }
686
687 /**
688 * Retrieve a single order discount object.
689 *
690 * @since 1.8.7
691 *
692 * @param array $discount Discount data.
693 * @param array $args Payment arguments.
694 *
695 * @return OrderLineItemDiscount
696 */
697 private function get_order_discount_object( array $discount, array $args ): OrderLineItemDiscount {
698
699 $order_discounts = new OrderLineItemDiscount();
700
701 $order_discounts->setName( $discount['name'] );
702 $order_discounts->setAmountMoney( new Money() );
703 $order_discounts->getAmountMoney()->setAmount( $discount['amount'] );
704 $order_discounts->getAmountMoney()->setCurrency( $args['currency'] );
705
706 return $order_discounts;
707 }
708
709 /**
710 * Set discounts to a create order request object.
711 *
712 * @since 1.8.7
713 *
714 * @param CreateOrderRequest $request Request to create an order.
715 * @param array $args Payment arguments.
716 *
717 * @return CreateOrderRequest
718 */
719 private function create_order_request_set_discounts( $request, array $args ): CreateOrderRequest {
720
721 $discounts = array();
722
723 if ( empty( $args['discounts'] ) ) {
724 return $request;
725 }
726
727 foreach ( (array) $args['discounts'] as $discount ) {
728
729 $discounts[] = $this->get_order_discount_object( $discount, $args );
730 }
731
732 if ( ! empty( $discounts ) ) {
733 $request->getOrder()->setDiscounts( $discounts );
734 }
735
736 return $request;
737 }
738
739 /**
740 * Set order to a create payment request object.
741 *
742 * @since 1.8.7
743 *
744 * @param CreatePaymentRequest $request Create payment request object.
745 * @param Order $order Order object.
746 *
747 * @return CreatePaymentRequest
748 */
749 private function create_payment_request_set_order( $request, $order ): CreatePaymentRequest {
750
751 $request->setOrderId( $order->getId() );
752 $request->setLocationId( $order->getLocationId() );
753
754 $amount = $order->getTotalMoney()->getAmount();
755 $currency = $order->getTotalMoney()->getCurrency();
756
757 $request->getAmountMoney()->setAmount( $amount );
758 $request->getAmountMoney()->setCurrency( $currency );
759
760 if ( ! Charitable_Square_Helpers::is_license_ok() && Charitable_Square_Helpers::is_application_fee_supported( $currency ) ) {
761 $request->setAppFeeMoney( new Money() );
762 $request->getAppFeeMoney()->setAmount( (int) ( round( $amount * 0.03 ) ) );
763 $request->getAppFeeMoney()->setCurrency( $currency );
764 }
765
766 return $request;
767 }
768
769 /**
770 * Retrieve a create payment request object.
771 *
772 * @since 1.8.7
773 *
774 * @return CreatePaymentRequest
775 */
776 private function get_create_payment_request_object(): CreatePaymentRequest {
777
778 $request = new CreatePaymentRequest(
779 array(
780 'sourceId' => $this->source_id,
781 'idempotencyKey' => $this->get_idempotency_key(),
782 )
783 );
784
785 $request->setAmountMoney( new Money() );
786
787 return $request;
788 }
789
790 /**
791 * Send a create order request object to API.
792 *
793 * @since 1.8.7
794 *
795 * @param CreateOrderRequest $request Create order request object.
796 *
797 * @return Order|null
798 */
799 private function send_create_order_request( $request ) {
800 try {
801 $this->response = $this->handle_rate_limiting(
802 function () use ( $request ) {
803 if ( charitable_is_debug( 'square' ) ) {
804 // phpcs:disable
805 error_log( 'Charitable_Square_API. send_create_order_request - inside rate limiting function' );
806 // phpcs:enable
807 }
808 return $this->client->orders->create( $request );
809 }
810 );
811
812 if ( $this->response->getErrors() ) {
813 $this->errors[] = esc_html__( 'Square fail: order was not created.', 'charitable' );
814 return null;
815 }
816
817 return $this->response->getOrder();
818
819 } catch ( \Square\Exceptions\SquareApiException $e ) {
820 if ( charitable_is_debug( 'square' ) ) {
821 // phpcs:disable
822 error_log( 'Charitable_Square_API. send_create_order_request - caught exception: ' . $e->getMessage() );
823 // phpcs:enable
824 }
825 $this->exception = $e;
826
827 // Get the error category and code from the exception message using regex.
828 $error_category = '';
829 $error_code = '';
830
831 // Look for "category": "SOME_CATEGORY" pattern.
832 if ( preg_match( '/"category":\s*"([^"]+)"/', $e->getMessage(), $category_matches ) ) {
833 $error_category = $category_matches[1];
834 if ( charitable_is_debug( 'square' ) ) {
835 // phpcs:disable
836 error_log( 'Charitable_Square_API. send_create_order_request - error category: ' . $error_category );
837 // phpcs:enable
838 }
839 }
840
841 // Look for "code": "SOME_CODE" pattern.
842 if ( preg_match( '/"code":\s*"([^"]+)"/', $e->getMessage(), $code_matches ) ) {
843 $error_code = $code_matches[1];
844 if ( charitable_is_debug( 'square' ) ) {
845 // phpcs:disable
846 error_log( 'Charitable_Square_API. send_create_order_request - error code: ' . $error_code );
847 // phpcs:enable
848 }
849 }
850
851 $this->errors[] = array(
852 'message' => esc_html__( 'Square fail: order was not created.', 'charitable' ),
853 'category' => $error_category,
854 'code' => $error_code,
855 );
856 return null;
857 }
858 }
859
860 /**
861 * Send an update order request object to API.
862 *
863 * @since 1.8.7
864 *
865 * @param string $order_id The ID of the order to update.
866 * @param UpdateOrderRequest $request Update order request object.
867 *
868 * @return Order|null
869 */
870 private function send_update_order_request( string $order_id, $request ) {
871 try {
872 $response = $this->handle_rate_limiting(
873 function () use ( $order_id, $request ) {
874 return $this->client->getOrdersApi()->updateOrder( $order_id, $request );
875 }
876 );
877
878 if ( ! $response->isSuccess() ) {
879 return null;
880 }
881
882 return $response->getResult()->getOrder();
883
884 } catch ( ApiException $e ) {
885 return null;
886 }
887 }
888
889 /**
890 * Handle rate limiting with exponential backoff.
891 *
892 * @since 1.8.7
893 *
894 * @param callable $api_call The API call to make.
895 * @param int $retry_count Current retry attempt.
896 * @return mixed The API response.
897 * @throws ApiException If the API call fails after all retries.
898 */
899 private function handle_rate_limiting( $api_call, $retry_count = 0 ) {
900 try {
901 return $api_call();
902 } catch ( ApiException $e ) {
903 // Check if this is a rate limit error (429).
904 if ( $e->getCode() === 429 && $retry_count < self::MAX_RETRIES ) {
905 // Calculate delay using exponential backoff: base_delay * (2^retry_count).
906 $delay = self::BASE_DELAY * pow( 2, $retry_count );
907
908 if ( charitable_is_debug( 'square' ) ) {
909 // phpcs:disable
910 error_log(
911 sprintf(
912 'Square API rate limited. Retry %d of %d. Waiting %d seconds.',
913 $retry_count + 1,
914 self::MAX_RETRIES,
915 $delay
916 )
917 );
918 // phpcs:enable
919 }
920
921 // Sleep for the calculated delay.
922 sleep( $delay );
923
924 // Retry the API call.
925 return $this->handle_rate_limiting( $api_call, $retry_count + 1 );
926 }
927
928 // If not a rate limit error or max retries reached, rethrow the exception.
929 throw $e;
930 }
931 }
932
933 /**
934 * Send a create payment request to API.
935 *
936 * @since 1.8.7
937 *
938 * @param CreatePaymentRequest $request Create payment request object.
939 *
940 * @return Payment|null
941 */
942 private function send_create_payment_request( $request ) {
943 try {
944 $this->response = $this->handle_rate_limiting(
945 function () use ( $request ) {
946 return $this->client->payments->create( $request );
947 }
948 );
949
950 if ( $this->response->getErrors() ) {
951 $this->errors[] = esc_html__( 'Square fail: payment was not processed.', 'charitable' );
952 $this->add_response_errors_message();
953 return null;
954 }
955
956 return $this->response->getPayment();
957
958 } catch ( ApiException $e ) {
959 $this->exception = $e;
960 $this->errors[] = esc_html__( 'Square fail: payment was not processed.', 'charitable' );
961 $this->add_response_errors_message( 'Exception' );
962 return null;
963 }
964 }
965
966 /**
967 * Perform a subscription transaction.
968 *
969 * @since 1.8.7
970 *
971 * @param array $args Payment arguments.
972 */
973 private function perform_subscription_transaction( array $args ) {
974
975 // Create a customer.
976 $customer_request = $this->prepare_create_customer_request( $args );
977 $customer = $this->send_create_customer_request( $customer_request );
978
979 if ( ! $customer instanceof Customer ) {
980 return;
981 }
982
983 // Create a customer card.
984 $card_request = $this->prepare_create_customer_card_request( $customer->getId(), $args );
985 $card = $this->send_create_customer_card_request( $card_request );
986
987 if ( ! $card instanceof Card ) {
988 return;
989 }
990
991 // Get a subscription plan.
992 $plan = $this->get_plan( $args );
993
994 if ( ! $plan instanceof CatalogObject ) {
995 return;
996 }
997
998 // Get a subscription plan variation.
999 $plan_variation = $this->get_plan_variation( $args, $plan );
1000
1001 if ( ! $plan_variation instanceof CatalogObject ) {
1002 return;
1003 }
1004
1005 // Create a subscription.
1006 $subscription_request = $this->prepare_create_subscription_request(
1007 $plan_variation->getValue()->getId(),
1008 $customer->getId(),
1009 $card->getId(),
1010 $args
1011 );
1012
1013 $result = $this->send_create_subscription_request( $subscription_request );
1014
1015 return $result;
1016 }
1017
1018 /**
1019 * Check if all required subscription arguments are present.
1020 *
1021 * @since 1.8.7
1022 *
1023 * @param array $args Arguments to check.
1024 */
1025 private function check_required_args_subscription( array $args ) {
1026
1027 if ( empty( $args['subscription']['plan_name'] ) ) {
1028 $this->errors[] = esc_html__( 'Missing subscription plan name.', 'charitable' );
1029 }
1030
1031 if ( empty( $args['subscription']['plan_variation_name'] ) ) {
1032 $this->errors[] = esc_html__( 'Missing subscription plan variation name.', 'charitable' );
1033 }
1034
1035 if ( empty( $args['subscription']['phase_cadence'] ) ) {
1036 $this->errors[] = esc_html__( 'Missing subscription cadence.', 'charitable' );
1037 }
1038
1039 if ( empty( $args['subscription']['customer']['first_name'] ) && empty( $args['subscription']['customer']['last_name'] ) ) {
1040 $this->errors[] = esc_html__( 'Missing customer name.', 'charitable' );
1041 }
1042
1043 if ( empty( $args['subscription']['customer']['email'] ) ) {
1044 $this->errors[] = esc_html__( 'Missing customer email.', 'charitable' );
1045 }
1046 }
1047
1048 /**
1049 * Prepare a create customer request object for sending to API.
1050 *
1051 * @since 1.8.7
1052 *
1053 * @param array $args Payment arguments.
1054 *
1055 * @return CreateCustomerRequest
1056 */
1057 private function prepare_create_customer_request( array $args ): CreateCustomerRequest {
1058
1059 $request = $this->get_create_customer_request_object();
1060
1061 $request->setEmailAddress( $args['subscription']['customer']['email'] );
1062
1063 if ( ! empty( $args['subscription']['customer']['first_name'] ) ) {
1064 $request->setGivenName( $args['subscription']['customer']['first_name'] );
1065 }
1066
1067 if ( ! empty( $args['subscription']['customer']['last_name'] ) ) {
1068 $request->setFamilyName( $args['subscription']['customer']['last_name'] );
1069 }
1070
1071 if ( ! empty( $args['subscription']['customer']['address'] ) ) {
1072 $address = $this->get_address_object( $args['subscription']['customer'] );
1073
1074 $request->setAddress( $address );
1075 }
1076
1077 return $request;
1078 }
1079
1080 /**
1081 * Prepare a create customer card request object for sending to API.
1082 *
1083 * @since 1.8.7
1084 *
1085 * @param string $customer_id Customer ID.
1086 * @param array $args Payment arguments.
1087 *
1088 * @return CreateCardRequest
1089 */
1090 private function prepare_create_customer_card_request( string $customer_id, array $args ): CreateCardRequest {
1091
1092 $request = new CreateCardRequest(
1093 array(
1094 'idempotencyKey' => $this->get_idempotency_key(),
1095 'sourceId' => $this->source_id,
1096 'card' => new Card(),
1097 )
1098 );
1099
1100 $request->getCard()->setCustomerId( $customer_id );
1101
1102 if ( ! empty( $args['subscription']['customer']['address'] ) ) {
1103 $address = $this->get_address_object( $args['subscription']['customer'] );
1104
1105 // For subscriptions: make sure that a postal code is not empty.
1106 // Otherwise, API error "The postal code doesn't match the one used for card nonce creation" is occur here.
1107 if ( ! empty( $address->getPostalCode() ) ) {
1108 $request->getCard()->setBillingAddress( $address );
1109 }
1110 }
1111
1112 if ( ! empty( $args['subscription']['card_name'] ) ) {
1113 $request->getCard()->setCardholderName( $args['subscription']['card_name'] );
1114 }
1115
1116 return $request;
1117 }
1118
1119 /**
1120 * Prepare a create subscription request object for sending to API.
1121 *
1122 * @since 1.8.7
1123 *
1124 * @param string $plan_id Plan ID.
1125 * @param string $customer_id Customer ID.
1126 * @param string $card_id Customer ID.
1127 * @param array $args Payment arguments.
1128 *
1129 * @return CreateSubscriptionRequest
1130 */
1131 private function prepare_create_subscription_request( string $plan_id, string $customer_id, string $card_id, array $args ) {
1132
1133 $request = $this->get_create_subscription_request_object( $plan_id, $customer_id, $args );
1134
1135 $request->setCardId( $card_id );
1136 $request->setSource( new SubscriptionSource() );
1137 $request->getSource()->setName( self::APP_NAME );
1138
1139 return $request;
1140 }
1141
1142 /**
1143 * Retrieve a create customer request object.
1144 *
1145 * @since 1.8.7
1146 *
1147 * @return CreateCustomerRequest
1148 */
1149 private function get_create_customer_request_object(): CreateCustomerRequest {
1150
1151 $request = new CreateCustomerRequest();
1152
1153 $request->setIdempotencyKey( $this->get_idempotency_key() );
1154
1155 return $request;
1156 }
1157
1158 /**
1159 * Prepare an update order request object for sending to API.
1160 *
1161 * @since 1.8.7
1162 *
1163 * @param Order $order Order object.
1164 *
1165 * @return UpdateOrderRequest
1166 */
1167 private function prepare_update_order_request( $order ): UpdateOrderRequest {
1168
1169 $request = $this->get_update_order_request_object( $order );
1170
1171 /**
1172 * Filter an update order request object.
1173 *
1174 * @since 1.8.7
1175 *
1176 * @param UpdateOrderRequest $request Update order request object.
1177 * @param Order $order Order object.
1178 * @param Api $api Api class instance.
1179 */
1180 return apply_filters( 'charitable_square_api_prepare_update_order_request', $request, $order, $this );
1181 }
1182
1183 /**
1184 * Send a create customer card request to API.
1185 *
1186 * @since 1.8.7
1187 *
1188 * @param CreateCardRequest $request Create card request object.
1189 *
1190 * @return Card|null
1191 */
1192 private function send_create_customer_card_request( $request ) {
1193 try {
1194 $raw_client = new RawClient(
1195 array(
1196 'baseUrl' => $this->test_mode ? \Square\Environments::Sandbox->value : \Square\Environments::Production->value,
1197 'headers' => array(
1198 'Authorization' => 'Bearer ' . $this->access_token,
1199 'Square-Version' => self::SQUARE_VERSION,
1200 'Content-Type' => 'application/json',
1201 ),
1202 )
1203 );
1204 $cards_client = new CardsClient( $raw_client );
1205
1206 $this->response = $this->handle_rate_limiting(
1207 function () use ( $cards_client, $request ) {
1208 return $cards_client->create( $request );
1209 }
1210 );
1211
1212 // Get the card from the response.
1213 $card = $this->response->getCard();
1214
1215 if ( ! $card ) {
1216 $this->errors[] = esc_html__( 'Square fail: customer card was not created.', 'charitable' );
1217 return null;
1218 }
1219
1220 return $card;
1221
1222 } catch ( ApiException $e ) {
1223 $this->exception = $e;
1224 $this->errors[] = esc_html__( 'Square fail: customer card was not created.', 'charitable' );
1225 return null;
1226 }
1227 }
1228
1229 /**
1230 * Prepare a create payment request object for sending to API.
1231 *
1232 * @since 1.8.7
1233 *
1234 * @param Order $order Order object.
1235 * @param array $args Payment arguments.
1236 *
1237 * @return CreatePaymentRequest
1238 */
1239 private function prepare_create_payment_request( $order, array $args ): CreatePaymentRequest {
1240
1241 $request = $this->get_create_payment_request_object();
1242 $request = $this->create_payment_request_set_order( $request, $order );
1243
1244 if ( ! empty( $args['billing'] ) ) {
1245 $address = $this->get_address_object( $args['billing'] );
1246
1247 $request->setBillingAddress( $address );
1248 }
1249
1250 if ( ! empty( $args['buyer_email'] ) ) {
1251 $request->setBuyerEmailAddress( $args['buyer_email'] );
1252 }
1253
1254 if ( ! empty( $args['note'] ) ) {
1255 $request->setNote( $args['note'] );
1256 }
1257
1258 /**
1259 * Filter a create payment request object.
1260 *
1261 * @since 1.8.7
1262 *
1263 * @param CreateOrderRequest $request Create payment request object.
1264 * @param array $args Payment arguments.
1265 * @param Api $api Api class instance.
1266 */
1267 return apply_filters( 'charitable_square_api_prepare_create_payment_request', $request, $args, $this );
1268 }
1269
1270 /**
1271 * Send a create customer request to API.
1272 *
1273 * @since 1.8.7
1274 *
1275 * @param CreateCustomerRequest $request Create customer request object.
1276 *
1277 * @return Customer|null
1278 */
1279 private function send_create_customer_request( $request ) {
1280 try {
1281 $raw_client = new RawClient(
1282 array(
1283 'baseUrl' => $this->test_mode ? \Square\Environments::Sandbox->value : \Square\Environments::Production->value,
1284 'headers' => array(
1285 'Authorization' => 'Bearer ' . $this->access_token,
1286 'Square-Version' => self::SQUARE_VERSION,
1287 'Content-Type' => 'application/json',
1288 ),
1289 )
1290 );
1291 $customers_client = new CustomersClient( $raw_client );
1292
1293 $this->response = $this->handle_rate_limiting(
1294 function () use ( $customers_client, $request ) {
1295 return $customers_client->create( $request );
1296 }
1297 );
1298
1299 // Get the customer from the response.
1300 $customer = $this->response->getCustomer();
1301
1302 if ( ! $customer ) {
1303 $this->errors[] = esc_html__( 'Square fail: customer was not created.', 'charitable' );
1304 return null;
1305 }
1306
1307 return $customer;
1308
1309 } catch ( ApiException $e ) {
1310 $this->exception = $e;
1311 $this->errors[] = esc_html__( 'Square fail: customer was not created.', 'charitable' );
1312 return null;
1313 }
1314 }
1315
1316 /**
1317 * Get subscription plan.
1318 *
1319 * @since 1.8.7
1320 *
1321 * @param array $args Payment arguments.
1322 *
1323 * @return CatalogObject|null
1324 */
1325 private function get_plan( array $args ) {
1326
1327 // Search a subscription plan.
1328 $search_plan_request = $this->get_search_catalog_request_object( 'SUBSCRIPTION_PLAN', $args['subscription']['plan_variation_name'] );
1329 $plan = $this->send_search_catalog_request( $search_plan_request );
1330
1331 // Create a subscription plan if it's not exists.
1332 if ( ! $plan ) {
1333 $create_plan_request = $this->get_create_plan_request_object( $args );
1334 $plan = $this->send_create_plan_request( $create_plan_request );
1335 }
1336
1337 return $plan;
1338 }
1339
1340 /**
1341 * Send a create subscription plan request to API.
1342 *
1343 * @since 1.8.7
1344 *
1345 * @param UpsertCatalogObjectRequest $request Create subscription plan request object.
1346 *
1347 * @return CatalogObject|null
1348 */
1349 private function send_create_plan_request( $request ) {
1350 try {
1351 $this->response = $this->handle_rate_limiting(
1352 function () use ( $request ) {
1353 return $this->client->catalog->object->upsert( $request );
1354 }
1355 );
1356
1357 if ( ! $this->response->getCatalogObject() ) {
1358 $this->errors[] = esc_html__( 'Square fail: subscription plan was not created.', 'charitable' );
1359 return null;
1360 }
1361
1362 return $this->response->getCatalogObject();
1363
1364 } catch ( ApiException $e ) {
1365 $this->exception = $e;
1366 $this->errors[] = esc_html__( 'Square fail: subscription plan was not created.', 'charitable' );
1367 return null;
1368 }
1369 }
1370
1371 /**
1372 * Send a create subscription plan variations request to API.
1373 *
1374 * @since 1.8.7
1375 *
1376 * @param UpsertCatalogObjectRequest $request Create subscription plan request object.
1377 *
1378 * @return CatalogObject|null
1379 */
1380 private function send_create_plan_variations_request( $request ) {
1381 try {
1382 $this->response = $this->handle_rate_limiting(
1383 function () use ( $request ) {
1384 return $this->client->catalog->object->upsert( $request );
1385 }
1386 );
1387
1388 if ( ! $this->response->getCatalogObject() ) {
1389 $this->errors[] = esc_html__( 'Square fail: subscription plan variation was not created.', 'charitable' );
1390 return null;
1391 }
1392
1393 return $this->response->getCatalogObject();
1394
1395 } catch ( ApiException $e ) {
1396 $this->exception = $e;
1397 $this->errors[] = esc_html__( 'Square fail: subscription plan variation was not created.', 'charitable' );
1398 return null;
1399 }
1400 }
1401
1402 /**
1403 * Send a create subscription request to API.
1404 *
1405 * @since 1.8.7
1406 *
1407 * @param CreateSubscriptionRequest $request Create subscription request object.
1408 *
1409 * @return Subscription|null
1410 */
1411 private function send_create_subscription_request( $request ) {
1412 try {
1413 $this->response = $this->handle_rate_limiting(
1414 function () use ( $request ) {
1415 return $this->client->subscriptions->create( $request );
1416 }
1417 );
1418
1419 if ( ! $this->response->getSubscription() ) {
1420 $this->errors[] = esc_html__( 'Square fail: something went wrong during subscription process.', 'charitable' );
1421 return null;
1422 }
1423
1424 return $this->response->getSubscription();
1425
1426 } catch ( ApiException $e ) {
1427 $this->exception = $e;
1428 $this->errors[] = esc_html__( 'Square fail: something went wrong during subscription process.', 'charitable' );
1429 return null;
1430 }
1431 }
1432
1433 /**
1434 * Retrieve a create plan request object.
1435 *
1436 * @since 1.8.7
1437 *
1438 * @param array $args Payment arguments.
1439 *
1440 * @return UpsertCatalogObjectRequest
1441 */
1442 private function get_create_plan_request_object( array $args ) {
1443
1444 $request = new UpsertCatalogObjectRequest(
1445 array(
1446 'idempotencyKey' => $this->get_idempotency_key(),
1447 'object' => CatalogObject::subscriptionPlan(
1448 new CatalogObjectSubscriptionPlan(
1449 array(
1450 'id' => '#plan_' . uniqid(), // Square requires IDs to start with # for new objects.
1451 'subscriptionPlanData' => new CatalogSubscriptionPlan(
1452 array(
1453 'name' => $args['subscription']['plan_name'],
1454 )
1455 ),
1456 )
1457 )
1458 ),
1459 )
1460 );
1461
1462 $request->getObject()->asSubscriptionPlan()->getSubscriptionPlanData()->setAllItems( true );
1463
1464 return $request;
1465 }
1466
1467 /**
1468 * Get subscription plan variation.
1469 *
1470 * @since 1.8.7
1471 *
1472 * @param array $args Payment arguments.
1473 * @param CatalogObject $plan Plan object.
1474 *
1475 * @return CatalogObject|null
1476 */
1477 private function get_plan_variation( array $args, $plan ) {
1478
1479 // Search a subscription plan.
1480 $search_plan_request = $this->get_search_catalog_request_object( 'SUBSCRIPTION_PLAN_VARIATION', $args['subscription']['plan_name'] );
1481 $plan_variation = $this->send_search_catalog_request( $search_plan_request );
1482
1483 // Create a subscription plan if it's not exists.
1484 if ( ! $plan_variation ) {
1485 $create_plan_variations_request = $this->get_create_plan_variations_request_object( $args, $plan );
1486 $plan_variation = $this->send_create_plan_variations_request( $create_plan_variations_request );
1487 }
1488
1489 return $plan_variation;
1490 }
1491
1492 /**
1493 * Retrieve a create plan variation request object.
1494 *
1495 * @since 1.8.7
1496 *
1497 * @param array $args Payment arguments.
1498 * @param CatalogObject $plan Plan object.
1499 *
1500 * @return UpsertCatalogObjectRequest
1501 */
1502 private function get_create_plan_variations_request_object( array $args, $plan ): UpsertCatalogObjectRequest {
1503
1504 if ( charitable_is_debug( 'square' ) ) {
1505 // phpcs:disable
1506 error_log( 'get_create_plan_variations_request_object - args' );
1507 error_log( print_r( $args, true ) );
1508 error_log( 'get_create_plan_variations_request_object - plan' );
1509 error_log( print_r( $plan, true ) );
1510 // phpcs:enable
1511 }
1512
1513 $request = new UpsertCatalogObjectRequest(
1514 array(
1515 'idempotencyKey' => $this->get_idempotency_key(),
1516 'object' => CatalogObject::subscriptionPlanVariation(
1517 new CatalogObjectSubscriptionPlanVariation(
1518 array(
1519 'id' => '#variation_' . uniqid(),
1520 'subscriptionPlanVariationData' => new CatalogSubscriptionPlanVariation(
1521 array(
1522 'name' => $args['subscription']['plan_variation_name'],
1523 'phases' => array(
1524 new SubscriptionPhase(
1525 array(
1526 'cadence' => $args['subscription']['phase_cadence']['value'],
1527 'pricing' => new SubscriptionPricing(
1528 array(
1529 'type' => 'STATIC',
1530 'priceMoney' => new Money(
1531 array(
1532 'amount' => $args['amount'],
1533 'currency' => $args['currency'],
1534 )
1535 ),
1536 )
1537 ),
1538 )
1539 ),
1540 ),
1541 'subscriptionPlanId' => $plan->asSubscriptionPlan()->getId(),
1542 )
1543 ),
1544 )
1545 )
1546 ),
1547 )
1548 );
1549
1550 return $request;
1551 }
1552
1553 /**
1554 * Retrieve a create subscription request object.
1555 *
1556 * @since 1.8.7
1557 *
1558 * @param string $plan_id Plan ID.
1559 * @param string $customer_id Customer ID.
1560 * @param array $args Payment arguments.
1561 *
1562 * @return CreateSubscriptionRequest
1563 */
1564 private function get_create_subscription_request_object( string $plan_id, string $customer_id, array $args ): CreateSubscriptionRequest {
1565 $request = new CreateSubscriptionRequest(
1566 array(
1567 'locationId' => $this->location_id,
1568 'customerId' => $customer_id,
1569 'planVariationId' => $plan_id,
1570 'idempotencyKey' => $this->get_idempotency_key(),
1571 'cardId' => $args['card_id'] ?? null,
1572 'startDate' => $args['start_date'] ?? null,
1573 'canceledDate' => $args['canceled_date'] ?? null,
1574 'taxPercentage' => $args['tax_percentage'] ?? null,
1575 'timezone' => $args['timezone'] ?? null,
1576 )
1577 );
1578
1579 return $request;
1580 }
1581
1582 /**
1583 * Retrieve a search catalog request object.
1584 *
1585 * @since 1.8.7
1586 *
1587 * @param string $type Object type.
1588 * @param string $name Object name.
1589 *
1590 * @return SearchCatalogObjectsRequest
1591 */
1592 private function get_search_catalog_request_object( string $type, string $name ): SearchCatalogObjectsRequest {
1593
1594 $request = new SearchCatalogObjectsRequest();
1595
1596 $request->setObjectTypes( array( $type ) );
1597 $request->setLimit( 1 );
1598 $request->setQuery( new CatalogQuery() );
1599 $request->getQuery()->setExactQuery(
1600 new CatalogQueryExact(
1601 array(
1602 'attributeName' => 'name',
1603 'attributeValue' => $name,
1604 )
1605 )
1606 );
1607
1608 return $request;
1609 }
1610
1611 /**
1612 * Send a search catalog request to API.
1613 *
1614 * @since 1.8.7
1615 *
1616 * @param SearchCatalogObjectsRequest $request Search subscription plan request object.
1617 *
1618 * @return CatalogObject|null
1619 */
1620 private function send_search_catalog_request( $request ) {
1621 try {
1622 $this->response = $this->handle_rate_limiting(
1623 function () use ( $request ) {
1624 return $this->client->catalog->search( $request );
1625 }
1626 );
1627
1628 $objects = $this->response->getObjects();
1629 if ( empty( $objects ) ) {
1630 $this->errors[] = esc_html__( 'Square fail: no catalog objects found.', 'charitable' );
1631 return null;
1632 }
1633
1634 return $objects;
1635
1636 } catch ( ApiException $e ) {
1637 $this->exception = $e;
1638 $this->errors[] = esc_html__( 'Square fail: something went wrong during catalog search.', 'charitable' );
1639 return null;
1640 }
1641 }
1642
1643 /**
1644 * Process a subscription cancellation.
1645 *
1646 * @since 1.8.7
1647 *
1648 * @param string $subscription_id The ID of the subscription to cancel.
1649 * @return bool True if the subscription was cancelled successfully, false otherwise.
1650 */
1651 public function process_subscription_cancellation( string $subscription_id ) {
1652 $this->check_connection();
1653
1654 if ( $this->errors ) {
1655 if ( charitable_is_debug( 'square' ) ) {
1656 // phpcs:disable
1657 error_log( 'process_subscription_cancellation - errors' );
1658 error_log( print_r( $this->errors, true ) );
1659 // phpcs:enable
1660 }
1661 return false;
1662 }
1663
1664 try {
1665 // Create cancel request.
1666 $request = new CancelSubscriptionsRequest(
1667 array(
1668 'subscriptionId' => $subscription_id,
1669 )
1670 );
1671
1672 if ( charitable_is_debug( 'square' ) ) {
1673 // phpcs:disable
1674 error_log( 'process_subscription_cancellation - request' );
1675 error_log( print_r( $request, true ) );
1676 // phpcs:enable
1677 }
1678
1679 // Send the request to Square API.
1680 $response = $this->client->subscriptions->cancel( $request );
1681
1682 if ( charitable_is_debug( 'square' ) ) {
1683 // phpcs:disable
1684 error_log( 'process_subscription_cancellation - response' );
1685 error_log( print_r( $response, true ) );
1686 // phpcs:enable
1687 }
1688
1689 // Check if cancellation was successful by looking for errors.
1690 if ( $response->getErrors() === null || empty( $response->getErrors() ) ) {
1691 // No errors means success.
1692 return $response;
1693 } else {
1694 // Log errors.
1695 $this->add_response_errors_message( 'SUBSCRIPTION_CANCELLATION' );
1696 return false;
1697 }
1698 } catch ( \Exception $e ) {
1699 $this->exception = $e;
1700 $this->errors[] = esc_html__( 'Square fail: subscription was not cancelled.', 'charitable' );
1701 return false;
1702 }
1703 }
1704
1705 /**
1706 * Refund payment.
1707 *
1708 * @since 1.8.7
1709 *
1710 * @param string $payment_id Payment ID.
1711 * @param array $args Payment data.
1712 */
1713 public function refund_payment( string $payment_id, array $args ) {
1714
1715 try {
1716 $request = new RefundPaymentRequest(
1717 array(
1718 'idempotencyKey' => $this->get_idempotency_key(),
1719 'paymentId' => $payment_id,
1720 'amountMoney' => ( new Money() )->setAmount( $args['amount'] )->setCurrency( $args['currency'] ),
1721 'reason' => $args['reason'],
1722 )
1723 );
1724
1725 $this->response = $this->client->refunds->refundPayment( $request );
1726
1727 if ( $this->response->getErrors() ) {
1728 $this->errors[] = esc_html__( 'Square fail: refund was not processed.', 'charitable' );
1729 return false;
1730 }
1731
1732 return $this->response->getRefund();
1733
1734 } catch ( ApiException $e ) {
1735 $this->exception = $e;
1736 $this->errors[] = esc_html__( 'Square fail: refund was not processed.', 'charitable' );
1737 return false;
1738 }
1739 }
1740
1741
1742 /**
1743 * Prepare and retrieve an address object.
1744 *
1745 * @since 1.8.7
1746 *
1747 * @param array $data Address data.
1748 *
1749 * @return Address
1750 */
1751 private function get_address_object( array $data ): Address {
1752
1753 $address = new Address();
1754
1755 // The empty country value may occur API errors.
1756 if ( ! empty( $data['address']['country'] ) ) {
1757 $address->setAddressLine1( $data['address']['address1'] );
1758 $address->setAddressLine2( $data['address']['address2'] );
1759 $address->setLocality( $data['address']['city'] );
1760 $address->setAdministrativeDistrictLevel1( $data['address']['state'] );
1761 $address->setPostalCode( $data['address']['postal'] );
1762 $address->setCountry( $data['address']['country'] );
1763 }
1764
1765 if ( ! empty( $data['first_name'] ) ) {
1766 $address->setFirstName( $data['first_name'] );
1767 }
1768
1769 if ( ! empty( $data['last_name'] ) ) {
1770 $address->setLastName( $data['last_name'] );
1771 }
1772
1773 return $address;
1774 }
1775
1776
1777 /**
1778 * Retrieve information of all locations of a business.
1779 *
1780 * @since 1.8.7
1781 *
1782 * @return array|Location|null
1783 */
1784 public function get_locations() {
1785
1786 try {
1787 $this->response = $this->client->locations->list();
1788
1789 if ( $this->response->getErrors() ) {
1790 if ( charitable_is_debug( 'square' ) ) {
1791 // phpcs:disable
1792 error_log( 'get_locations - errors' );
1793 error_log( $this->response->getErrors() );
1794 // phpcs:enable
1795 }
1796 return null;
1797 }
1798
1799 return $this->response->getLocations();
1800
1801 } catch ( \Square\Exceptions\SquareApiException $e ) {
1802
1803 $this->exception = $e;
1804 return null;
1805 }
1806 }
1807
1808 /**
1809 * Return the Square business location ID
1810 *
1811 * @since 1.8.7
1812 *
1813 * @param string $mode The test mode... either 'test' or 'live'.
1814 * @return string|false
1815 */
1816 public function get_location_id( $mode = 'test' ) {
1817 if ( charitable_is_debug( 'square' ) ) {
1818 // phpcs:disable
1819 error_log( 'get_location_id - the mode is: ' . $mode );
1820 // phpcs:enable
1821 }
1822
1823 if ( 'test' !== $mode && 'live' !== $mode ) {
1824 if ( charitable_is_debug( 'square' ) ) {
1825 // phpcs:disable
1826 error_log( 'get_location_id - bad mode' . $mode );
1827 // phpcs:enable
1828 }
1829 return false;
1830 }
1831
1832 if ( charitable_is_debug( 'square' ) ) {
1833 // phpcs:disable
1834 error_log( 'get_location_id - checking if square_legacy_settings is set' );
1835 // phpcs:enable
1836 }
1837
1838 // Are we overridng with legacy settings?
1839 if ( charitable_square_legacy_mode() ) {
1840 $location_id = charitable_get_option( 'gateways_square', $mode . '_location_id' );
1841 if ( charitable_is_debug( 'square' ) ) {
1842 // phpcs:disable
1843 error_log( 'get_location_id - square_legacy_settings is set to true, so the location id is' );
1844 error_log( print_r( $location_id, true ) );
1845 // phpcs:enable
1846 }
1847 return $location_id;
1848 }
1849
1850 $location_id = get_option( 'charitable_square_location_id_' . $mode );
1851
1852 if ( empty( $location_id ) ) {
1853 return false;
1854 }
1855
1856 return $location_id;
1857 }
1858
1859 /**
1860 * Set the location ID for a specific mode.
1861 *
1862 * @since 1.8.7
1863 *
1864 * @param string $location_id The location ID to set.
1865 * @param string $mode The mode to set the location ID for (test or live).
1866 * @return bool True on success.
1867 */
1868 public function set_location_id( $location_id = '', $mode = 'test' ) {
1869
1870 update_option( 'charitable_square_location_id_' . $mode, $location_id );
1871
1872 return true;
1873 }
1874
1875 /**
1876 * Set the location currency for a specific mode.
1877 *
1878 * @since 1.8.7
1879 *
1880 * @param string $currency The currency code to set.
1881 * @param string $mode The mode to set the currency for (test or live).
1882 * @return bool True on success.
1883 */
1884 public function set_location_currency( $currency = '', $mode = 'test' ) {
1885 update_option( 'charitable_square_location_currency_' . $mode, $currency );
1886 return true;
1887 }
1888
1889 /**
1890 * Retrieve last API call response and display error messages.
1891 *
1892 * @since 1.14.0
1893 *
1894 * @param string $type Type of error message.
1895 */
1896 private function add_response_errors_message( string $type = 'API' ) {
1897
1898 $errors = $this->get_response_errors();
1899
1900 if ( empty( $errors ) ) {
1901 return;
1902 }
1903
1904 $key = ( $type === 'Exception' ) ? 'message' : 'detail';
1905
1906 foreach ( $errors as $error ) {
1907 $message = $error[ $key ] ?? '';
1908
1909 if ( empty( $error['code'] ) || empty( $message ) ) {
1910 return;
1911 }
1912
1913 $this->errors[] = esc_html( $type ) . ': (' . esc_html( $error['code'] ) . ') ' . esc_html( $message );
1914 }
1915 }
1916
1917 /**
1918 * Retrieve API errors.
1919 *
1920 * @since 1.8.7
1921 *
1922 * @return array|null
1923 */
1924 public function get_errors() {
1925
1926 return $this->errors;
1927 }
1928
1929 /**
1930 * Retrieve last API call errors if are exist.
1931 *
1932 * @since 1.8.7
1933 *
1934 * @return array
1935 */
1936 public function get_response_errors(): array {
1937
1938 if ( $this->response instanceof \Square\Legacy\Http\ApiResponse && ! $this->response->isSuccess() ) {
1939 $errors = array();
1940
1941 foreach ( (array) $this->response->getErrors() as $error ) {
1942 $errors[] = $error->jsonSerialize();
1943 }
1944
1945 return $errors;
1946 }
1947
1948 if ( $this->exception instanceof ApiException ) {
1949 return array(
1950 'code' => $this->exception->getCode(),
1951 'message' => $this->exception->getMessage(),
1952 );
1953 }
1954
1955 return array();
1956 }
1957
1958 /**
1959 * Retrieve an idempotency key.
1960 *
1961 * @since 1.8.7
1962 *
1963 * @link https://developer.squareup.com/docs/working-with-apis/idempotency
1964 *
1965 * @return string
1966 */
1967 private function get_idempotency_key(): string {
1968
1969 return uniqid( '', false );
1970 }
1971
1972 /**
1973 * Delete transients by mode.
1974 *
1975 * @since 1.8.7
1976 *
1977 * @param string $mode Square mode.
1978 */
1979 public static function detete_transients( string $mode ) {
1980
1981 delete_transient( 'charitable_square_account_' . $mode );
1982 delete_transient( 'charitable_square_active_locations_' . $mode );
1983 }
1984
1985 /**
1986 * Return the API key to use for the current mode.
1987 * This function might be deprecated in the future.
1988 *
1989 * @since 1.8.7
1990 * @param string $type live|test.
1991 *
1992 * @return string|false
1993 */
1994 public function get_access_token( $type = 'test' ) {
1995
1996 if ( charitable_is_debug( 'square' ) ) {
1997 // phpcs:disable
1998 error_log( 'starting to get the access token' );
1999 // phpcs:enable
2000 }
2001
2002 if ( 'test' !== $type && 'live' !== $type ) {
2003 return false;
2004 }
2005
2006 $settings = charitable_get_option( 'gateways_square' );
2007
2008 if ( charitable_is_debug( 'square' ) ) {
2009 // phpcs:disable
2010 error_log( 'got charitablesquare settings, now get the access token' );
2011 // phpcs:enable
2012 }
2013
2014 if ( empty( $settings ) ) {
2015 if ( charitable_is_debug( 'square' ) ) {
2016 // phpcs:disable
2017 error_log( 'get_access_token - settings are empty' );
2018 // phpcs:enable
2019 }
2020 return false;
2021 }
2022
2023 if ( empty( $settings[ $type ]['access_token'] ) ) {
2024 if ( charitable_is_debug( 'square' ) ) {
2025 // phpcs:disable
2026 error_log( 'get_access_token - access token is empty' );
2027 // phpcs:enable
2028 }
2029 return false;
2030 }
2031
2032 $access_token = trim( $settings[ $type ]['access_token'] );
2033
2034 if ( empty( $access_token ) ) {
2035 if ( charitable_is_debug( 'square' ) ) {
2036 // phpcs:disable
2037 error_log( 'get_access_token - access token is empty' );
2038 // phpcs:enable
2039 }
2040 return false;
2041 }
2042
2043 if ( charitable_is_debug( 'square' ) ) {
2044 // phpcs:disable
2045 error_log( 'get_access_token - got the access token and it is ' . $access_token );
2046 // phpcs:enable
2047 }
2048
2049 return $access_token;
2050 }
2051
2052 /**
2053 * Checks whether an Access Token is set.
2054 *
2055 * @since 1.8.7
2056 *
2057 * @return boolean
2058 */
2059 public function has_valid_access_token() {
2060 $token_type = $this->test_mode ? 'test' : 'live';
2061
2062 /* We're missing an Access Token. */
2063 if ( false === $this->{$token_type . '_access_token'} ) {
2064 return false;
2065 }
2066
2067 /**
2068 * If we have made an API call, we record whether there
2069 * was an issue with using the Access Token. This will catch
2070 * incorrect or invalid Access Tokens.
2071 */
2072 if ( isset( $this->access_token_validated ) ) {
2073 return $this->access_token_validated;
2074 }
2075
2076 return true;
2077 }
2078
2079 /**
2080 * Return the response to the most recent request.
2081 *
2082 * @since 1.8.7
2083 *
2084 * @return WP_Error|?
2085 */
2086 public function get_last_response() {
2087 return $this->last_response;
2088 }
2089
2090 /**
2091 * Checks whether the API key is valid, based on API call response.
2092 *
2093 * @since 1.8.7
2094 *
2095 * @return boolean
2096 */
2097 private function access_token_validated() {
2098 return '401' !== wp_remote_retrieve_response_code( $this->last_response );
2099 }
2100
2101 /**
2102 * Returns whether the most recent request failed.
2103 *
2104 * @since 1.8.7
2105 *
2106 * @return boolean
2107 */
2108 private function is_failed_request() {
2109 return is_wp_error( $this->last_response ) || '2' !== substr( wp_remote_retrieve_response_code( $this->last_response ), 0, 1 );
2110 }
2111
2112 /**
2113 * Renew the access token using the refresh token.
2114 *
2115 * @since 1.8.7
2116 *
2117 * @param string $refresh_token The refresh token to use for renewal.
2118 * @return array|false The response from Square API or false on failure.
2119 */
2120 public function renew_token( $refresh_token ) {
2121 if ( charitable_is_debug( 'square' ) ) {
2122 // phpcs:disable
2123 error_log( 'check for removal - starting renew_token' );
2124 // phpcs:enable
2125 }
2126
2127 if ( empty( $refresh_token ) ) {
2128 return false;
2129 }
2130
2131 $oauth_url = $this->test_mode ? 'https://connect.squareupsandbox.com/oauth2/token' : 'https://connect.squareup.com/oauth2/token';
2132
2133 $args = array(
2134 'grant_type' => 'refresh_token',
2135 'refresh_token' => $refresh_token,
2136 );
2137
2138 $request_args = array(
2139 'method' => 'POST',
2140 'timeout' => 30,
2141 'redirection' => 5,
2142 'httpversion' => '1.0',
2143 'blocking' => true,
2144 'headers' => array(
2145 'Content-Type' => 'application/json',
2146 ),
2147 'body' => wp_json_encode( $args ),
2148 );
2149
2150 $response = wp_remote_post( $oauth_url, $request_args );
2151
2152 if ( charitable_is_debug( 'square' ) ) {
2153 // phpcs:disable
2154 error_log( 'renew_token - response' );
2155 error_log( print_r( $response, true ) );
2156 // phpcs:enable
2157 }
2158
2159 if ( is_wp_error( $response ) ) {
2160 return false;
2161 }
2162
2163 $body = wp_remote_retrieve_body( $response );
2164 $data = json_decode( $body, true );
2165
2166 if ( empty( $data ) || ! isset( $data['access_token'] ) ) {
2167 return false;
2168 }
2169
2170 return $data;
2171 }
2172
2173 /**
2174 * Check the status of the access token.
2175 *
2176 * @since 1.8.7
2177 *
2178 * @return array {
2179 * Token status information.
2180 *
2181 * @type bool $is_valid Whether the token is valid.
2182 * @type string $message Error message if token is invalid.
2183 * }
2184 */
2185 public function check_token_status() {
2186 try {
2187 $response = $this->client->oAuth->retrieveTokenStatus();
2188
2189 if ( empty( $response->getErrors() ) ) {
2190 return array(
2191 'is_valid' => true,
2192 'message' => '',
2193 );
2194 }
2195
2196 $errors = $response->getErrors();
2197 $error = reset( $errors );
2198
2199 if ( charitable_is_debug( 'square' ) ) {
2200 // phpcs:disable
2201 error_log( 'Square token status check error: ' . print_r( $error, true ) );
2202 // phpcs:enable
2203 }
2204
2205 return array(
2206 'is_valid' => false,
2207 'message' => $error->getDetail() ?? __( 'Your Square connection has expired.', 'charitable' ),
2208 );
2209
2210 } catch ( \Exception $e ) {
2211 if ( charitable_is_debug( 'square' ) ) {
2212 // phpcs:disable
2213 error_log( 'Square token status check exception: ' . $e->getMessage() );
2214 // phpcs:enable
2215 }
2216
2217 return array(
2218 'is_valid' => false,
2219 'message' => $e->getMessage(),
2220 );
2221 }
2222 }
2223
2224 /**
2225 * Revoke an access token.
2226 *
2227 * @param string $client_id The client ID.
2228 * @param string $access_token The access token to revoke.
2229 * @param string $merchant_id The merchant ID.
2230 * @return array{success: bool, error?: string}
2231 */
2232 public function revoke_token( string $client_id, string $access_token, string $merchant_id ): array {
2233 try {
2234 $request = new \Square\Models\RevokeTokenRequest(
2235 array(
2236 'clientId' => $client_id,
2237 'accessToken' => $access_token,
2238 'merchantId' => $merchant_id,
2239 )
2240 );
2241
2242 // Get the application secret from settings.
2243 $settings = charitable_get_option( 'square_settings', array() );
2244 $application_secret = isset( $settings['application_secret'] ) ? $settings['application_secret'] : '';
2245
2246 if ( empty( $application_secret ) ) {
2247 return array(
2248 'success' => false,
2249 'error' => __( 'Square application secret is not configured.', 'charitable' ),
2250 );
2251 }
2252
2253 // Set the authorization header with the application secret.
2254 $options = array(
2255 'headers' => array(
2256 'Authorization' => 'Client ' . $application_secret,
2257 ),
2258 );
2259
2260 $response = $this->client->getOAuthApi()->revokeToken( $request, $options );
2261
2262 if ( charitable_is_debug( 'square' ) ) {
2263 // phpcs:disable
2264 error_log( 'Square token revocation response: ' . print_r( $response, true ) );
2265 // phpcs:enable
2266 }
2267
2268 return array( 'success' => true );
2269 } catch ( \Exception $e ) {
2270 if ( charitable_is_debug( 'square' ) ) {
2271 // phpcs:disable
2272 error_log( 'Square token revocation error: ' . $e->getMessage() );
2273 // phpcs:enable
2274 }
2275
2276 return array(
2277 'success' => false,
2278 'error' => $e->getMessage(),
2279 );
2280 }
2281 }
2282
2283 /**
2284 * Get merchant details from Square API.
2285 *
2286 * @since 1.8.7
2287 *
2288 * @return string|null The merchant name or null if not found.
2289 */
2290 public function get_merchant_name() {
2291 // Get merchant ID from instance property or settings.
2292 $merchant_id = $this->merchant_id;
2293
2294 if ( empty( $merchant_id ) ) {
2295 $square_settings = charitable_get_option( 'gateways_square', array() );
2296 $mode = $this->test_mode ? 'test' : 'live';
2297 $merchant_id = ! empty( $square_settings[ $mode ]['merchant_id'] ) ? $square_settings[ $mode ]['merchant_id'] : '';
2298 }
2299
2300 if ( empty( $merchant_id ) ) {
2301 return null;
2302 }
2303
2304 $merchant_name = get_transient( 'charitable_square_merchant_name_' . $merchant_id );
2305
2306 if ( false !== $merchant_name ) {
2307 return $merchant_name;
2308 }
2309
2310 try {
2311 $request = new \Square\Merchants\Requests\GetMerchantsRequest(
2312 array(
2313 'merchantId' => $merchant_id,
2314 )
2315 );
2316 $response = $this->client->merchants->get( $request );
2317
2318 if ( $response && $response->getMerchant() ) {
2319 $merchant = $response->getMerchant();
2320 $merchant_name = $merchant->getBusinessName() ? $merchant->getBusinessName() : $merchant->getMainLocationName();
2321
2322 if ( $merchant_name ) {
2323 set_transient( 'charitable_square_merchant_name_' . $merchant_id, $merchant_name, DAY_IN_SECONDS );
2324 return $merchant_name;
2325 }
2326 }
2327 } catch ( \Exception $e ) {
2328 if ( charitable_is_debug( 'square' ) ) {
2329 // phpcs:disable
2330 error_log( 'Square API Error: ' . $e->getMessage() );
2331 // phpcs:enable
2332 }
2333 }
2334
2335 return null;
2336 }
2337 }
2338