PluginProbe
Route ‑ Shipping Protection / 2.4.15
Route ‑ Shipping Protection v2.4.15
2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.2.4 2.2.5 2.2.6 2.2.8 2.2.9 2.3.0 2.3.1 2.3.2 2.3.3 All 151 releases
routeapp / includes / class-routeapp-api-client.php

class-routeapp-api-client.php in Route ‑ Shipping Protection 2.4.15, at includes/class-routeapp-api-client.php

858 lines 28.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Routeapp API Client Class
4 *
5 * @link https://route.com/
6 * @since 1.0.0
7 *
8 * @package Routeapp
9 * @subpackage Routeapp/includes
10 */
11
12 class Routeapp_API_Client
13 {
14 /**
15 * API base endpoint v1
16 */
17 const API_ENDPOINT_V1 = 'https://api.route.com/v1/';
18
19 /**
20 * API stage base endpoint v1
21 */
22 const API_STAGE_ENDPOINT_V1 = 'https://api-stage.route.com/v1/';
23
24 /**
25 * API base endpoint v2
26 */
27 const API_ENDPOINT_V2 = 'https://api.route.com/v2/';
28
29 /**
30 * API stage base endpoint v2
31 */
32 const API_STAGE_ENDPOINT_V2 = 'https://api-stage.route.com/v2/';
33
34 /**
35 * Merchant ID Option name
36 */
37 const ROUTEAPP_MERCHANT_ID = 'routeapp_merchant_id';
38
39 /**
40 * The v1 API URL
41 * @var string
42 */
43 private $_api_url;
44
45 /**
46 * The v2 API URL
47 * @var string
48 */
49 private $_api_url_v2;
50
51 /**
52 * The WooCommerce Merchant Key
53 * @var string
54 */
55 private $_public_token;
56
57 /**
58 * The WooCommerce Merchant Key
59 * @var string
60 */
61 private $_secret_token;
62
63
64 /**
65 * Merchant
66 */
67 private $_merchant;
68
69 /**
70 * Data used for API Call
71 * @var array
72 */
73 protected $_extraData = [];
74
75 /**
76 * Cache API calls
77 */
78 private $_cachedApiCallsSessionKey = 'route_get_quote';
79
80 private static $instances = [];
81
82 /**
83 * Default contructor
84 * @param string $public_token The consumer key
85 * @param string $secret_token The consumer key
86 */
87 public function __construct($public_token = null, $secret_token = null)
88 {
89 $this->set_public_token($public_token);
90 $this->set_secret_token($secret_token);
91
92 $custom_env = getenv('ROUTEAPP_ENVIRONMENT_ENDPOINT');
93 if (is_null($custom_env) || !$custom_env) {
94 $custom_env = isset($_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT']) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : '';
95 }
96 if ($custom_env == 'stage') {
97 $this->_api_url = rtrim($this->_api_url ?? '', '/') . self::API_STAGE_ENDPOINT_V1;
98 $this->_api_url_v2 = rtrim($this->_api_url_v2 ?? '', '/') . self::API_STAGE_ENDPOINT_V2;
99 } else {
100 $this->_api_url = rtrim($this->_api_url ?? '', '/') . self::API_ENDPOINT_V1;
101 $this->_api_url_v2 = rtrim($this->_api_url_v2 ?? '', '/') . self::API_ENDPOINT_V2;
102 }
103 }
104
105 /**
106 * Singletons should not be cloneable.
107 */
108 protected function __clone()
109 {}
110
111 public static function getInstance()
112 {
113 $cls = static::class;
114 if (!isset(static::$instances[$cls])) {
115 static::$instances[$cls] = new static;
116 }
117 return static::$instances[$cls];
118 }
119
120 /**
121 * Set the public token
122 * @param string $token
123 */
124 public function set_public_token($token)
125 {
126 $this->_public_token = $token;
127 }
128
129 /**
130 * Set the secret token
131 * @param string $token
132 */
133 public function set_secret_token($token)
134 {
135 $this->_secret_token = $token;
136 }
137
138 /**
139 * Get the public token
140 * @return string string
141 */
142 public function get_public_token()
143 {
144 return !empty($this->_public_token) ? $this->_public_token : get_option('routeapp_public_token');
145 }
146
147 public function get_cache_api_session_key()
148 {
149 return $this->_cachedApiCallsSessionKey;
150 }
151
152 /**
153 * Get the secret token
154 * @return string string
155 */
156 public function get_secret_token()
157 {
158 return !empty($this->_secret_token) ? $this->_secret_token : get_option('routeapp_secret_token');
159 }
160
161 /**
162 * Get the user token
163 * @return string string
164 */
165 public function get_user_token()
166 {
167 return get_option('routeapp_user_token');
168 }
169 /**
170 * Get the user id
171 * @return string string
172 */
173 public function get_user_id()
174 {
175 return get_option('routeapp_user_id');
176 }
177
178 /**
179 * Get current quote price based on subtotal
180 * Optimized to store only essential data in session
181 * @param $cartRef
182 * @param $cartTotal
183 * @param $currency
184 * @param $cartItems
185 * @return array|mixed
186 */
187 public function get_quote($cartRef, $cartTotal, $currency, $cartItems)
188 {
189 $currency = !$currency ? get_woocommerce_currency() : $currency;
190 $cartTotal = !is_null($cartTotal) && $cartTotal > 0 ? $cartTotal : 0;
191 $merchant_id = $this->get_merchant_id();
192
193 //empty subtotal or merchant_id just return zero
194 if ($cartTotal==0 || !$merchant_id) return ['body' => json_encode(['premium' => ['amount' => '0']])];
195
196 //check values on cache
197 $cached = false;
198 $key = $this->get_cache_api_session_key() . '-' . $cartRef;
199 if (WC()->session) {
200 $cached = WC()->session->get($key);
201 }
202 if ($cached) {
203 if (time() - $cached['createdAt'] > 1800) {
204 //if creation date is more than 30 minutes, we unset it
205 WC()->session->__unset($key);
206 $lastCalledMade = $key . '-latest';
207 WC()->session->__unset($lastCalledMade);
208 // Clean up all old entries when we find an expired one
209 $this->_cleanup_old_quote_sessions();
210 } else {
211 return $cached['result'];
212 }
213 }
214
215 // Make API call
216 $api_response = $this->_make_private_api_call('quotes', array(
217 'merchant_id' => $merchant_id,
218 'cart' => [
219 'cart_ref' => strval($cartRef),
220 'covered' => [
221 'currency' => strval($currency),
222 'amount' => strval($cartTotal)
223 ],
224 'cart_items' => $cartItems,
225 ],
226 ), 'POST', 'v2');
227
228 // Extract only essential data from API response to minimize session storage
229 $essential_data = $this->_extract_essential_quote_data($api_response);
230
231 // Store only essential data in session (not the full HTTP response)
232 if (WC()->session) {
233 // Clean up old entries BEFORE adding new one to enforce limit
234 // This ensures we don't exceed max_entries even when adding a new quote
235 $this->_cleanup_old_quote_sessions();
236
237 $created_at = time();
238 $cached = array(
239 'createdAt' => $created_at,
240 'result' => $essential_data
241 );
242 WC()->session->set($key, $cached);
243
244 // Store latest quote data (also minimal)
245 // Format: array with 'body' key for compatibility with routeapp_save_quote_to_order
246 $lastCalledMade = $key . '-latest';
247 WC()->session->set($lastCalledMade, $essential_data);
248
249 // Track this key for cleanup and limit total entries
250 $this->_track_quote_session_key($key, $created_at);
251
252 // Clean up again after adding to ensure limit is strictly enforced
253 // This handles the case where we had exactly max_entries before adding
254 $this->_cleanup_old_quote_sessions();
255 }
256
257 return $essential_data;
258 }
259
260 /**
261 * Extract only essential data from API response
262 * Prevents storing full HTTP response objects in session
263 * Stores only: id, premium.amount, premium.currency, payment_responsible.type, payment_responsible.ToggleState
264 *
265 * @param array|WP_Error $api_response The full API response from wp_remote_request
266 * @return array Minimal quote data in expected format (compatible with existing code)
267 */
268 private function _extract_essential_quote_data($api_response)
269 {
270 // Handle errors - return in expected format
271 if (is_wp_error($api_response)) {
272 return array(
273 'response' => array('code' => 500),
274 'body' => json_encode(array('premium' => array('amount' => '0')))
275 );
276 }
277
278 // Extract body from response
279 $response_code = wp_remote_retrieve_response_code($api_response);
280 $response_body = wp_remote_retrieve_body($api_response);
281
282 // If API call failed, return original format for error handling
283 if ($response_code !== 200 && $response_code !== 201) {
284 return $api_response; // Return original for error handling
285 }
286
287 // Parse JSON body
288 $body_data = json_decode($response_body, true);
289
290 if (!$body_data || empty($body_data)) {
291 return array(
292 'response' => array('code' => $response_code),
293 'body' => json_encode(array('premium' => array('amount' => '0')))
294 );
295 }
296
297 $essential_quote = array();
298
299 // Extract essential fields
300 if (isset($body_data['id'])) {
301 $essential_quote['id'] = $body_data['id'];
302 }
303
304 if (isset($body_data['premium'])) {
305 $essential_quote['premium'] = array(
306 'currency' => isset($body_data['premium']['currency']) ? $body_data['premium']['currency'] : 'USD',
307 'amount' => isset($body_data['premium']['amount']) ? $body_data['premium']['amount'] : '0'
308 );
309 }
310
311 if (isset($body_data['payment_responsible'])) {
312 $payment = $body_data['payment_responsible'];
313 $essential_quote['payment_responsible'] = array(
314 'type' => isset($payment['type']) ? $payment['type'] : 'paid_by_merchant'
315 );
316
317 // Convert to boolean ToggleState for compatibility with existing code
318 if (array_key_exists('toggle_state', $payment)) {
319 $toggle_state_value = $payment['toggle_state'];
320 // Convert "checked" -> true, "unchecked" -> false
321 $essential_quote['payment_responsible']['ToggleState'] = ($toggle_state_value === 'checked' || $toggle_state_value === true || $toggle_state_value === 1);
322 } elseif (array_key_exists('ToggleState', $payment)) {
323 // Fallback for camelCase format (if API changes)
324 $essential_quote['payment_responsible']['ToggleState'] = $payment['ToggleState'];
325 }
326 }
327
328 // Return in expected format (compatible with routeapp_get_quote_from_api)
329 return array(
330 'response' => array('code' => $response_code),
331 'body' => json_encode($essential_quote, JSON_FORCE_OBJECT)
332 );
333 }
334
335 /**
336 * Track quote session keys for cleanup management
337 *
338 * @param string $key Session key
339 * @param int $created_at Timestamp
340 */
341 private function _track_quote_session_key($key, $created_at)
342 {
343 if (!WC()->session) {
344 return;
345 }
346
347 $tracker_key = $this->get_cache_api_session_key() . '_keys';
348 $tracked_keys = WC()->session->get($tracker_key);
349
350 if (!is_array($tracked_keys)) {
351 $tracked_keys = array();
352 }
353
354 // Add current key to tracker (avoid duplicates)
355 $key_exists = false;
356 foreach ($tracked_keys as $index => $tracked) {
357 if (isset($tracked['key']) && $tracked['key'] === $key) {
358 $tracked_keys[$index]['time'] = $created_at; // Update timestamp
359 $key_exists = true;
360 break;
361 }
362 }
363
364 if (!$key_exists) {
365 $tracked_keys[] = array('key' => $key, 'time' => $created_at);
366 }
367
368 WC()->session->set($tracker_key, $tracked_keys);
369 }
370
371 /**
372 * Clean up old quote session entries to prevent session bloat
373 * Removes entries older than 30 minutes and limits total entries per session
374 * This prevents accumulation of hundreds of quote entries
375 */
376 private function _cleanup_old_quote_sessions()
377 {
378 if (!WC()->session) {
379 return;
380 }
381
382 $cache_prefix = $this->get_cache_api_session_key();
383 $current_time = time();
384 $max_age = 1800; // 30 minutes
385 $max_entries = 5; // Maximum number of quote entries per session (reduced from potential hundreds)
386
387 // Get tracked keys
388 $tracker_key = $cache_prefix . '_keys';
389 $tracked_keys = WC()->session->get($tracker_key);
390
391 if (!is_array($tracked_keys) || empty($tracked_keys)) {
392 return;
393 }
394
395 // Clean up old entries (expired) and verify they still exist in session
396 $valid_keys = array();
397 foreach ($tracked_keys as $key_with_timestamp) {
398 if (!is_array($key_with_timestamp) || !isset($key_with_timestamp['key']) || !isset($key_with_timestamp['time'])) {
399 continue;
400 }
401
402 $key = $key_with_timestamp['key'];
403 $created_at = $key_with_timestamp['time'];
404 $age = $current_time - $created_at;
405
406 // Check if entry still exists in session (might have been manually removed)
407 $session_entry = WC()->session->get($key);
408
409 if ($age > $max_age || !$session_entry) {
410 // Remove expired or missing entry
411 WC()->session->__unset($key);
412 WC()->session->__unset($key . '-latest');
413 } else {
414 // Keep valid entry
415 $valid_keys[] = $key_with_timestamp;
416 }
417 }
418
419 // Always sort by time (newest first) to prepare for limiting
420 usort($valid_keys, function($a, $b) {
421 return $b['time'] - $a['time'];
422 });
423
424 // Limit total entries (keep most recent) - STRICTLY enforce max_entries
425 if (count($valid_keys) > $max_entries) {
426 // Keep only max_entries (most recent)
427 $keys_to_keep = array_slice($valid_keys, 0, $max_entries);
428
429 // Remove excess entries (older ones beyond limit)
430 $keys_to_remove = array_slice($valid_keys, $max_entries);
431 foreach ($keys_to_remove as $excess_entry) {
432 if (isset($excess_entry['key'])) {
433 $excess_key = $excess_entry['key'];
434 WC()->session->__unset($excess_key);
435 WC()->session->__unset($excess_key . '-latest');
436 }
437 }
438
439 // Update tracker with only kept keys (maintain sorted order)
440 $valid_keys = $keys_to_keep;
441 }
442
443 // Always update tracker to maintain correct order and remove any orphaned entries
444 WC()->session->set($tracker_key, $valid_keys);
445 }
446
447 /**
448 * Create the order shipment, currently only status update suported by API
449 * @param integer $tracking_id
450 * @param array $data
451 * @return mixed|json string
452 */
453 public function create_shipment($tracking_id, $data = array())
454 {
455 if (empty($tracking_id)) return false;
456 return $this->_make_private_api_call('shipments', array(
457 'tracking_number' => $this->sanitize_value($tracking_id),
458 'source_order_id' => $data['source_order_id'],
459 'source_product_ids' => $data['source_product_ids'],
460 'courier_id' => $this->sanitize_value($data['courier_id']),
461 ), 'POST');
462 }
463
464 /**
465 *
466 * Sanitize shipstation tracking numbers. Moved to here from the
467 * class-routeapp-shipstation.php script because it is sometimes
468 * getting bypassed and orders are coming through with "-(SHIPSTATION)"
469 * at the end. Also added a more specific check for both a shipstation
470 * prefix and suffix WITH the dash since shipstation has moved the
471 * shipstation label to the back AND orders were coming through with
472 * either a leading or trailing "-"
473 *
474 * @param $value
475 * @return array|string
476 */
477
478 private function sanitize_value($value) {
479 $value = str_replace(['-(SHIPSTATION)', '(SHIPSTATION)-', '(Shipstation)'], '', $value);
480 $value = str_replace('.', '', $value);
481 $value = trim($value);
482 return $value;
483 }
484
485 /**
486 * Get the order shipment
487 * @param integer $tracking_id
488 * @param integer $order_id
489 * @param array $data
490 * @return mixed|json string
491 */
492 public function get_shipment($tracking_id, $order_id)
493 {
494 if (empty($tracking_id) || empty($order_id)) return false;
495 return $this->_make_private_api_call('shipments/' . $tracking_id . '?source_order_id=' . $order_id);
496 }
497
498 /**
499 * Update the order shipment, currently only status update suported by API
500 * @param integer $tracking_id
501 * @param integer $order_id
502 * @param array $data
503 * @return mixed|json string
504 */
505 public function update_shipment($tracking_id, $order_id, $data = array())
506 {
507 if (empty($tracking_id) || empty($order_id)) return false;
508 return $this->_make_private_api_call('shipments/' . $tracking_id . '?source_order_id=' . $order_id, array(
509 'source_order_id' => $data['source_order_id'],
510 'source_product_ids' => $data['source_product_ids'],
511 'courier_id' => $data['courier_id'],
512 ), 'POST');
513 }
514
515 /**
516 * Cancel the order shipment, currently only status update suported by API
517 * @param integer $tracking_id
518 * @param integer $order_id
519 * @param array $data
520 * @return mixed|json string
521 */
522 public function cancel_shipment($tracking_id, $data = array())
523 {
524 if (empty($tracking_id)) return false;
525 return $this->_make_private_api_call('shipments/' . $tracking_id . '/cancel' . '?source_order_id=' . $data['source_order_id'], array(
526 'source_order_id' => $data['source_order_id'],
527 'source_product_ids' => $data['source_product_ids'],
528 ), 'POST');
529 }
530
531 /**
532 * Create the order, currently only status update suported by API
533 * @param integer $data
534 * @return mixed|json string
535 */
536 public function create_order($data)
537 {
538 return $this->_make_private_api_call('orders', $data, 'POST', 'v2');
539 }
540
541 /**
542 * Get the order
543 * @param integer $source_order_id
544 * @return mixed|json string
545 */
546 public function get_order($source_order_id)
547 {
548 return $this->_make_private_api_call('orders/' . $source_order_id, 'GET');
549 }
550
551 /**
552 * Update the order, currently only status update suported by API
553 * @param integer $data
554 * @return mixed|json string
555 */
556 public function update_order($order_id, $data)
557 {
558 return $this->_make_private_api_call('orders/' . $order_id, $data, 'POST', 'v2');
559 }
560
561 /**
562 * Cancel the order, currently only status update suported by API
563 * @param integer $order_id
564 * @return mixed|json string
565 */
566 public function cancel_order($order_id)
567 {
568 return $this->_make_private_api_call('orders/' . $order_id . '/cancel', array(), 'POST');
569 }
570
571 /**
572 * Get user billing status settings
573 * @param integer $order_id
574 * @return mixed|json string
575 */
576 public function get_billing()
577 {
578 return $this->_make_private_api_call('billing', array(), 'GET');
579 }
580
581 /**
582 * Get the Route Merchant ID
583
584 * @return mixed
585 */
586 public function get_merchant_id()
587 {
588 return get_option(self::ROUTEAPP_MERCHANT_ID);
589 }
590
591 /**
592 * Get the Route Merchant ID
593 *
594 * @param $merchant_id
595 * @param $blog_id
596 *
597 * @return mixed
598 */
599 public function set_merchant_id($merchant_id, $blog_id = null){
600 if (isset($blog_id) && is_multisite()) {
601 return update_blog_option($blog_id, self::ROUTEAPP_MERCHANT_ID, $merchant_id);
602 }
603 return update_option(self::ROUTEAPP_MERCHANT_ID , $merchant_id);
604 }
605
606 public static function get_route_public_instance(){
607 global $routeapp_public;
608 return $routeapp_public;
609 }
610
611 /**
612 * Get merchant
613
614 * @return mixed
615 */
616 public function get_merchant()
617 {
618 if (!empty($this->_merchant)) {
619 return $this->_merchant;
620 }
621
622 $endpoint = 'merchants';
623 $merchantResponse = $this->get_merchant_id() ?
624 $this->_make_private_api_call($endpoint . '/' . $this->get_merchant_id(), array(), 'GET') :
625 $this->_make_private_api_call($endpoint, array(), 'GET');
626
627 try {
628 $response_code = wp_remote_retrieve_response_code($merchantResponse);
629
630 if ( is_wp_error($merchantResponse) || $response_code != 200 ) {
631 if ($response_code !== 403) {
632 $errorMsg = is_wp_error($merchantResponse) ? $merchantResponse->get_error_message() : $response_code;
633 throw new Exception("Route API Error while getting merchant data: " . $errorMsg);
634 }
635
636 $merchantResponse = $this->_make_private_api_call($endpoint, array(), 'GET');
637 }
638 } catch(Exception $exception) {
639 $routeapp_public = self::get_route_public_instance();
640 $routeapp_public->routeapp_log($exception, $this->_extraData);
641 return false;
642 }
643
644 if ($merchantResponse) {
645 if ($merchantResponse["body"]) {
646 $body = json_decode($merchantResponse["body"]);
647 $merchant = is_array($body) ? $body[0] : $body;
648
649 if ($merchant) {
650 $this->_merchant = $merchant;
651
652 if (empty($this->get_merchant_id()) || (isset($merchant->id) && $merchant->id !== $this->get_merchant_id())) {
653 $this->set_merchant_id($merchant->id, get_current_blog_id());
654 }
655 return $this->_merchant;
656 }
657 }
658
659 }
660 }
661
662 /**
663 * Create user account
664 * @param array $data
665 * @return mixed|json string
666 */
667 public function create_user($data) {
668 return $this->_make_private_api_call( 'users', $data, 'POST' );
669 }
670
671 /**
672 * Create user account
673 * @param $username
674 * @param $password
675 * @return mixed|json string
676 */
677 public function login_user($username, $password) {
678 return $this->_make_private_api_call( 'login', [
679 "username" => $username,
680 "password" => $password
681 ], 'POST' );
682 }
683
684 /**
685 * Create merchant account
686 * @param array $data
687 * @return mixed|json string
688 */
689 public function create_merchant($data) {
690 return $this->_make_private_api_call_using_user_token( 'merchants', $data, 'POST' );
691 }
692
693 /**
694 * Get merchant account by user
695 * @return mixed|json string
696 */
697 public function get_merchants() {
698 return $this->_make_private_api_call_using_user_token( "users/" . $this->get_user_id() . "/merchants", [], 'GET' );
699 }
700
701 /**
702 * Get activate account link at Route API
703 *
704 * @param array $email
705 * @return mixed|json string
706 */
707 public function activate_account($email) {
708 return $this->_make_private_api_call( 'activate_account', $email, 'POST' );
709 }
710
711 /**
712 * Get asset settings
713 * @param $apiHost
714 * @return mixed|json string
715 */
716 public function asset_settings($apiHost) {
717 return $this->_make_public_api_call("asset-settings/$apiHost", array(), 'GET');
718 }
719
720 /**
721 * Update the account status at Route API
722 *
723 * @return mixed|json string
724 */
725 public function update_merchant_status($status) {
726 $endpoint = 'merchants/' . $this->get_merchant_id();
727 $params = ['status' => $status];
728 return $this->_make_private_api_call( $endpoint, $params, 'POST' );
729 }
730
731 /*
732 * Make the call to the API
733 * @param string $endpoint
734 * @param array $params
735 * @param string $method
736 * @param string $version
737 * @return mixed|json string
738 */
739 private function _make_api_call($token, $endpoint, $params = array(), $method = 'GET', $version='v1')
740 {
741 $url = $version=='v1' ? $this->_api_url : $this->_api_url_v2;
742 $url.= $endpoint;
743
744 $extraData = array(
745 'params' => $params,
746 'method' => $method,
747 'endpoint' => $url
748 );
749 $this->_extraData = $extraData;
750
751 $headers = [
752 'Content-Type' => 'application/json',
753 'token' => $token,
754 ];
755 if ($version=='v2') {
756 $headers['Protect-Widget-Version'] = 'route-widget-core';
757 }
758 //platform
759 $headers['platform'] = 'woocommerce';
760
761 //woocommerce + wordpress version
762 $wooVersion = '';
763 if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) &&
764 defined('WC_VERSION') ) {
765 $wooVersion = WC_VERSION;
766 }
767 $wordpressVersion = '';
768 if (function_exists('get_bloginfo')) {
769 $wordpressVersion = get_bloginfo('version');
770 }
771 $headers['platform_version'] = 'WooCommerce: ' . $wooVersion . ' WordPress: ' . $wordpressVersion;
772
773 //route module version
774 $module_version= defined('ROUTEAPP_VERSION') ? ROUTEAPP_VERSION :'';
775 $headers['module_version'] = $module_version;
776
777 $args = array(
778 'timeout' => 6,
779 'method' => $method,
780 'headers' => $headers,
781 'body' => $method === 'POST' ? json_encode($params) : null
782 );
783
784 return wp_remote_request($url, $args);
785 }
786
787 private function _make_public_api_call($endpoint, $params = array(), $method = 'GET', $version='v1')
788 {
789 return $this->_make_api_call($this->get_public_token(), $endpoint, $params, $method, $version);
790 }
791
792 protected function _make_private_api_call($endpoint, $params = array(), $method = 'GET', $version='v1')
793 {
794 return $this->_make_api_call($this->get_secret_token(), $endpoint, $params, $method, $version);
795 }
796
797 protected function _make_private_api_call_using_user_token($endpoint, $params = array(), $method = 'GET', $version='v1')
798 {
799 return $this->_make_api_call($this->get_user_token(), $endpoint, $params, $method, $version);
800 }
801
802 /**
803 * Exchange a one-time token for merchant data (no merchant API auth; Route v1/otp/verify).
804 *
805 * @param string $token OTP from Route Dashboard after signing in with platformUrl.
806 * @return array|\WP_Error Merchant payload with id, public_api_key, prod_api_secret, store_domain on success.
807 */
808 public function verify_otp( $token ) {
809 $url = $this->_api_url . 'otp/verify';
810 $headers = array(
811 'Content-Type' => 'application/json',
812 'platform' => 'woocommerce',
813 );
814 $wooVersion = '';
815 if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ), true ) &&
816 defined( 'WC_VERSION' ) ) {
817 $wooVersion = WC_VERSION;
818 }
819 $wordpressVersion = function_exists( 'get_bloginfo' ) ? get_bloginfo( 'version' ) : '';
820 $headers['platform_version'] = 'WooCommerce: ' . $wooVersion . ' WordPress: ' . $wordpressVersion;
821 $headers['module_version'] = defined( 'ROUTEAPP_VERSION' ) ? ROUTEAPP_VERSION : '';
822
823 $args = array(
824 'timeout' => 15,
825 'method' => 'POST',
826 'headers' => $headers,
827 'body' => wp_json_encode( array( 'token' => $token ) ),
828 );
829
830 $response = wp_remote_request( $url, $args );
831
832 if ( is_wp_error( $response ) ) {
833 return $response;
834 }
835
836 $code = wp_remote_retrieve_response_code( $response );
837 $body_raw = wp_remote_retrieve_body( $response );
838 $data = json_decode( $body_raw, true );
839
840 if ( 200 !== (int) $code ) {
841 $message = is_array( $data ) && ! empty( $data['error'] ) ? $data['error'] : 'OTP verification failed';
842 return new \WP_Error( 'route_otp_verify_failed', $message, array( 'status' => $code ) );
843 }
844
845 if ( ! is_array( $data ) ) {
846 return new \WP_Error( 'route_otp_invalid_response', 'Unexpected response from Route API' );
847 }
848
849 $result = isset( $data['result'] ) && is_array( $data['result'] ) ? $data['result'] : $data;
850
851 if ( empty( $result['id'] ) || empty( $result['public_api_key'] ) || empty( $result['prod_api_secret'] ) ) {
852 return new \WP_Error( 'route_otp_incomplete', 'OTP response missing merchant credentials' );
853 }
854
855 return $result;
856 }
857 }
858