PluginProbe
Route ‑ Shipping Protection / 2.4.17
Route ‑ Shipping Protection v2.4.17
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.17, at includes/class-routeapp-api-client.php

904 lines 30.0 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 $payload = array(
457 'tracking_number' => $this->sanitize_value($tracking_id),
458 'source_order_id' => isset($data['source_order_id']) ? $data['source_order_id'] : null,
459 'source_product_ids' => isset($data['source_product_ids']) ? $data['source_product_ids'] : array(),
460 'courier_id' => $this->sanitize_value(isset($data['courier_id']) ? $data['courier_id'] : ''),
461 );
462 $response = $this->_make_private_api_call('shipments', $payload, 'POST');
463
464 return $this->retry_create_shipment_with_parent_product_ids($payload, $response);
465 }
466
467 /**
468 * If the first create-shipment is rejected (HTTP 400, typically Invalid
469 * ProductIDs), retry once with parent product_ids from the Woo order.
470 * Those match webhook order-create when variation meta never reaches Route.
471 *
472 * @param array $payload Original shipment payload.
473 * @param array|\WP_Error $response First create-shipment response.
474 * @return array|\WP_Error
475 */
476 private function retry_create_shipment_with_parent_product_ids($payload, $response)
477 {
478 if ( ! class_exists('Routeapp_WooCommerce_Common_Tracking_Provider') ) {
479 return $response;
480 }
481
482 if ( Routeapp_WooCommerce_Common_Tracking_Provider::is_successful_shipment_create_response($response) ) {
483 return $response;
484 }
485
486 $code = 0;
487 if ( isset($response['response']['code']) ) {
488 $code = (int) $response['response']['code'];
489 } elseif ( function_exists('wp_remote_retrieve_response_code') ) {
490 $code = (int) wp_remote_retrieve_response_code($response);
491 }
492 if ( $code !== 400 || empty($payload['source_order_id']) ) {
493 return $response;
494 }
495
496 $parent_ids = Routeapp_WooCommerce_Common_Tracking_Provider::get_order_products_parent_ids($payload['source_order_id']);
497 if ( empty($parent_ids) ) {
498 return $response;
499 }
500
501 $original_ids = array_map('strval', (array) $payload['source_product_ids']);
502 if ( $original_ids === $parent_ids ) {
503 return $response;
504 }
505
506 $payload['source_product_ids'] = $parent_ids;
507 return $this->_make_private_api_call('shipments', $payload, 'POST');
508 }
509
510 /**
511 *
512 * Sanitize shipstation tracking numbers. Moved to here from the
513 * class-routeapp-shipstation.php script because it is sometimes
514 * getting bypassed and orders are coming through with "-(SHIPSTATION)"
515 * at the end. Also added a more specific check for both a shipstation
516 * prefix and suffix WITH the dash since shipstation has moved the
517 * shipstation label to the back AND orders were coming through with
518 * either a leading or trailing "-"
519 *
520 * @param $value
521 * @return array|string
522 */
523
524 private function sanitize_value($value) {
525 $value = str_replace(['-(SHIPSTATION)', '(SHIPSTATION)-', '(Shipstation)'], '', $value);
526 $value = str_replace('.', '', $value);
527 $value = trim($value);
528 return $value;
529 }
530
531 /**
532 * Get the order shipment
533 * @param integer $tracking_id
534 * @param integer $order_id
535 * @param array $data
536 * @return mixed|json string
537 */
538 public function get_shipment($tracking_id, $order_id)
539 {
540 if (empty($tracking_id) || empty($order_id)) return false;
541 return $this->_make_private_api_call('shipments/' . $tracking_id . '?source_order_id=' . $order_id);
542 }
543
544 /**
545 * Update the order shipment, currently only status update suported by API
546 * @param integer $tracking_id
547 * @param integer $order_id
548 * @param array $data
549 * @return mixed|json string
550 */
551 public function update_shipment($tracking_id, $order_id, $data = array())
552 {
553 if (empty($tracking_id) || empty($order_id)) return false;
554 return $this->_make_private_api_call('shipments/' . $tracking_id . '?source_order_id=' . $order_id, array(
555 'source_order_id' => $data['source_order_id'],
556 'source_product_ids' => $data['source_product_ids'],
557 'courier_id' => $data['courier_id'],
558 ), 'POST');
559 }
560
561 /**
562 * Cancel the order shipment, currently only status update suported by API
563 * @param integer $tracking_id
564 * @param integer $order_id
565 * @param array $data
566 * @return mixed|json string
567 */
568 public function cancel_shipment($tracking_id, $data = array())
569 {
570 if (empty($tracking_id)) return false;
571 return $this->_make_private_api_call('shipments/' . $tracking_id . '/cancel' . '?source_order_id=' . $data['source_order_id'], array(
572 'source_order_id' => $data['source_order_id'],
573 'source_product_ids' => $data['source_product_ids'],
574 ), 'POST');
575 }
576
577 /**
578 * Create the order, currently only status update suported by API
579 * @param integer $data
580 * @return mixed|json string
581 */
582 public function create_order($data)
583 {
584 return $this->_make_private_api_call('orders', $data, 'POST', 'v2');
585 }
586
587 /**
588 * Get the order
589 * @param integer $source_order_id
590 * @return mixed|json string
591 */
592 public function get_order($source_order_id)
593 {
594 return $this->_make_private_api_call('orders/' . $source_order_id, 'GET');
595 }
596
597 /**
598 * Update the order, currently only status update suported by API
599 * @param integer $data
600 * @return mixed|json string
601 */
602 public function update_order($order_id, $data)
603 {
604 return $this->_make_private_api_call('orders/' . $order_id, $data, 'POST', 'v2');
605 }
606
607 /**
608 * Cancel the order, currently only status update suported by API
609 * @param integer $order_id
610 * @return mixed|json string
611 */
612 public function cancel_order($order_id)
613 {
614 return $this->_make_private_api_call('orders/' . $order_id . '/cancel', array(), 'POST');
615 }
616
617 /**
618 * Get user billing status settings
619 * @param integer $order_id
620 * @return mixed|json string
621 */
622 public function get_billing()
623 {
624 return $this->_make_private_api_call('billing', array(), 'GET');
625 }
626
627 /**
628 * Get the Route Merchant ID
629
630 * @return mixed
631 */
632 public function get_merchant_id()
633 {
634 return get_option(self::ROUTEAPP_MERCHANT_ID);
635 }
636
637 /**
638 * Get the Route Merchant ID
639 *
640 * @param $merchant_id
641 * @param $blog_id
642 *
643 * @return mixed
644 */
645 public function set_merchant_id($merchant_id, $blog_id = null){
646 if (isset($blog_id) && is_multisite()) {
647 return update_blog_option($blog_id, self::ROUTEAPP_MERCHANT_ID, $merchant_id);
648 }
649 return update_option(self::ROUTEAPP_MERCHANT_ID , $merchant_id);
650 }
651
652 public static function get_route_public_instance(){
653 global $routeapp_public;
654 return $routeapp_public;
655 }
656
657 /**
658 * Get merchant
659
660 * @return mixed
661 */
662 public function get_merchant()
663 {
664 if (!empty($this->_merchant)) {
665 return $this->_merchant;
666 }
667
668 $endpoint = 'merchants';
669 $merchantResponse = $this->get_merchant_id() ?
670 $this->_make_private_api_call($endpoint . '/' . $this->get_merchant_id(), array(), 'GET') :
671 $this->_make_private_api_call($endpoint, array(), 'GET');
672
673 try {
674 $response_code = wp_remote_retrieve_response_code($merchantResponse);
675
676 if ( is_wp_error($merchantResponse) || $response_code != 200 ) {
677 if ($response_code !== 403) {
678 $errorMsg = is_wp_error($merchantResponse) ? $merchantResponse->get_error_message() : $response_code;
679 throw new Exception("Route API Error while getting merchant data: " . $errorMsg);
680 }
681
682 $merchantResponse = $this->_make_private_api_call($endpoint, array(), 'GET');
683 }
684 } catch(Exception $exception) {
685 $routeapp_public = self::get_route_public_instance();
686 $routeapp_public->routeapp_log($exception, $this->_extraData);
687 return false;
688 }
689
690 if ($merchantResponse) {
691 if ($merchantResponse["body"]) {
692 $body = json_decode($merchantResponse["body"]);
693 $merchant = is_array($body) ? $body[0] : $body;
694
695 if ($merchant) {
696 $this->_merchant = $merchant;
697
698 if (empty($this->get_merchant_id()) || (isset($merchant->id) && $merchant->id !== $this->get_merchant_id())) {
699 $this->set_merchant_id($merchant->id, get_current_blog_id());
700 }
701 return $this->_merchant;
702 }
703 }
704
705 }
706 }
707
708 /**
709 * Create user account
710 * @param array $data
711 * @return mixed|json string
712 */
713 public function create_user($data) {
714 return $this->_make_private_api_call( 'users', $data, 'POST' );
715 }
716
717 /**
718 * Create user account
719 * @param $username
720 * @param $password
721 * @return mixed|json string
722 */
723 public function login_user($username, $password) {
724 return $this->_make_private_api_call( 'login', [
725 "username" => $username,
726 "password" => $password
727 ], 'POST' );
728 }
729
730 /**
731 * Create merchant account
732 * @param array $data
733 * @return mixed|json string
734 */
735 public function create_merchant($data) {
736 return $this->_make_private_api_call_using_user_token( 'merchants', $data, 'POST' );
737 }
738
739 /**
740 * Get merchant account by user
741 * @return mixed|json string
742 */
743 public function get_merchants() {
744 return $this->_make_private_api_call_using_user_token( "users/" . $this->get_user_id() . "/merchants", [], 'GET' );
745 }
746
747 /**
748 * Get activate account link at Route API
749 *
750 * @param array $email
751 * @return mixed|json string
752 */
753 public function activate_account($email) {
754 return $this->_make_private_api_call( 'activate_account', $email, 'POST' );
755 }
756
757 /**
758 * Get asset settings
759 * @param $apiHost
760 * @return mixed|json string
761 */
762 public function asset_settings($apiHost) {
763 return $this->_make_public_api_call("asset-settings/$apiHost", array(), 'GET');
764 }
765
766 /**
767 * Update the account status at Route API
768 *
769 * @return mixed|json string
770 */
771 public function update_merchant_status($status) {
772 $endpoint = 'merchants/' . $this->get_merchant_id();
773 $params = ['status' => $status];
774 return $this->_make_private_api_call( $endpoint, $params, 'POST' );
775 }
776
777 /*
778 * Make the call to the API
779 * @param string $endpoint
780 * @param array $params
781 * @param string $method
782 * @param string $version
783 * @return mixed|json string
784 */
785 private function _make_api_call($token, $endpoint, $params = array(), $method = 'GET', $version='v1')
786 {
787 $url = $version=='v1' ? $this->_api_url : $this->_api_url_v2;
788 $url.= $endpoint;
789
790 $extraData = array(
791 'params' => $params,
792 'method' => $method,
793 'endpoint' => $url
794 );
795 $this->_extraData = $extraData;
796
797 $headers = [
798 'Content-Type' => 'application/json',
799 'token' => $token,
800 ];
801 if ($version=='v2') {
802 $headers['Protect-Widget-Version'] = 'route-widget-core';
803 }
804 //platform
805 $headers['platform'] = 'woocommerce';
806
807 //woocommerce + wordpress version
808 $wooVersion = '';
809 if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) &&
810 defined('WC_VERSION') ) {
811 $wooVersion = WC_VERSION;
812 }
813 $wordpressVersion = '';
814 if (function_exists('get_bloginfo')) {
815 $wordpressVersion = get_bloginfo('version');
816 }
817 $headers['platform_version'] = 'WooCommerce: ' . $wooVersion . ' WordPress: ' . $wordpressVersion;
818
819 //route module version
820 $module_version= defined('ROUTEAPP_VERSION') ? ROUTEAPP_VERSION :'';
821 $headers['module_version'] = $module_version;
822
823 $args = array(
824 'timeout' => 6,
825 'method' => $method,
826 'headers' => $headers,
827 'body' => $method === 'POST' ? json_encode($params) : null
828 );
829
830 return wp_remote_request($url, $args);
831 }
832
833 private function _make_public_api_call($endpoint, $params = array(), $method = 'GET', $version='v1')
834 {
835 return $this->_make_api_call($this->get_public_token(), $endpoint, $params, $method, $version);
836 }
837
838 protected function _make_private_api_call($endpoint, $params = array(), $method = 'GET', $version='v1')
839 {
840 return $this->_make_api_call($this->get_secret_token(), $endpoint, $params, $method, $version);
841 }
842
843 protected function _make_private_api_call_using_user_token($endpoint, $params = array(), $method = 'GET', $version='v1')
844 {
845 return $this->_make_api_call($this->get_user_token(), $endpoint, $params, $method, $version);
846 }
847
848 /**
849 * Exchange a one-time token for merchant data (no merchant API auth; Route v1/otp/verify).
850 *
851 * @param string $token OTP from Route Dashboard after signing in with platformUrl.
852 * @return array|\WP_Error Merchant payload with id, public_api_key, prod_api_secret, store_domain on success.
853 */
854 public function verify_otp( $token ) {
855 $url = $this->_api_url . 'otp/verify';
856 $headers = array(
857 'Content-Type' => 'application/json',
858 'platform' => 'woocommerce',
859 );
860 $wooVersion = '';
861 if ( in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ), true ) &&
862 defined( 'WC_VERSION' ) ) {
863 $wooVersion = WC_VERSION;
864 }
865 $wordpressVersion = function_exists( 'get_bloginfo' ) ? get_bloginfo( 'version' ) : '';
866 $headers['platform_version'] = 'WooCommerce: ' . $wooVersion . ' WordPress: ' . $wordpressVersion;
867 $headers['module_version'] = defined( 'ROUTEAPP_VERSION' ) ? ROUTEAPP_VERSION : '';
868
869 $args = array(
870 'timeout' => 15,
871 'method' => 'POST',
872 'headers' => $headers,
873 'body' => wp_json_encode( array( 'token' => $token ) ),
874 );
875
876 $response = wp_remote_request( $url, $args );
877
878 if ( is_wp_error( $response ) ) {
879 return $response;
880 }
881
882 $code = wp_remote_retrieve_response_code( $response );
883 $body_raw = wp_remote_retrieve_body( $response );
884 $data = json_decode( $body_raw, true );
885
886 if ( 200 !== (int) $code ) {
887 $message = is_array( $data ) && ! empty( $data['error'] ) ? $data['error'] : 'OTP verification failed';
888 return new \WP_Error( 'route_otp_verify_failed', $message, array( 'status' => $code ) );
889 }
890
891 if ( ! is_array( $data ) ) {
892 return new \WP_Error( 'route_otp_invalid_response', 'Unexpected response from Route API' );
893 }
894
895 $result = isset( $data['result'] ) && is_array( $data['result'] ) ? $data['result'] : $data;
896
897 if ( empty( $result['id'] ) || empty( $result['public_api_key'] ) || empty( $result['prod_api_secret'] ) ) {
898 return new \WP_Error( 'route_otp_incomplete', 'OTP response missing merchant credentials' );
899 }
900
901 return $result;
902 }
903 }
904