PluginProbe
Route ‑ Shipping Protection / 2.2.5
Route ‑ Shipping Protection v2.2.5
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 / public / class-routeapp-public.php

class-routeapp-public.php in Route ‑ Shipping Protection 2.2.5, at public/class-routeapp-public.php

1,578 lines 60.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The public-facing functionality of the plugin.
4 *
5 * @link https://route.com/
6 * @since 1.0.0
7 *
8 * @package Routeapp
9 * @subpackage Routeapp/public
10 */
11
12 use Automattic\WooCommerce\Utilities\OrderUtil;
13
14 /**
15 * The public-facing functionality of routeapp.
16 *
17 *
18 * @package Routeapp
19 * @subpackage Routeapp/public
20 * @author Route App <support@route.com>
21 */
22
23 class Routeapp_Public {
24
25 const HTTP_SUCCESS_CODE = 200;
26 const HTTP_BAD_REQUEST_CODE = 400;
27 const HTTP_CREATED_CODE = 201;
28 const HTTP_CONFLICT_CODE = 409;
29 const HTTP_NOT_FOUND = 404;
30 const HTTP_INTERNAL_SERVER_ERROR_CODE = 500;
31 const DEFAULT_MAX_USD_SUBTOTAL_ALLOWED = 5000;
32 const DEFAULT_MIN_USD_SUBTOTAL_ALLOWED = 0;
33 const ROUTE_LABEL = 'Route Shipping Protection';
34 const DEFAULT_CHECKOUT_WEBHOOK = 'woocommerce_checkout_before_order_review';
35 const ROUTE_PROTECTION_SKU = 'ROUTEINS';
36 const ALLOWED_QUOTE_TYPE = 'paid_by_customer';
37
38 /**
39 * The ID of this plugin.
40 *
41 * @since 1.0.0
42 * @access private
43 * @var string $plugin_name The ID of this plugin.
44 */
45 private $plugin_name;
46
47 /**
48 * The version of this plugin.
49 *
50 * @since 1.0.0
51 * @access private
52 * @var string $version The current version of this plugin.
53 */
54 private $version;
55
56 /**
57 * Routeapp API instance
58 * @since 1.0.0
59 * @access private
60 * @var string $routeapp_api Routeapp API Client
61 */
62 public $routeapp_api_client;
63
64
65 /**
66 * Route Shipment Tracking Module
67 */
68 private $routeapp_shipment_tracking;
69
70 /**
71 * Last Request
72 */
73 private $routeapp_last_request;
74
75 /**
76 * Route Logger
77 */
78 private $routeapp_logger;
79
80 /**
81 * Route Admin Fee
82 */
83 private $routeapp_admin_fee;
84
85 /**
86 * Route Setup
87 */
88 private $route_app_setup_helper;
89
90 /**
91 * Initialize the class and set its properties.
92 *
93 * @since 1.0.0
94 * @param string $plugin_name The name of the plugin.
95 * @param string $version The version of this plugin.
96 */
97 public function __construct($plugin_name, $version)
98 {
99
100 $this->plugin_name = $plugin_name;
101 $this->version = $version;
102 $this->routeapp_api_client = new Routeapp_API_Client($this->routeapp_get_public_token(), $this->routeapp_get_secret_token());
103 $this->routeapp_shipment_tracking = new Routeapp_Shipment_Tracking();
104 $this->routeapp_logger = new Routeapp_Logger_Sentry();
105 $this->routeapp_admin_fee = new Routeapp_Admin_Route_Fee();
106 $this->route_app_setup_helper = new Route_Setup();
107 $this->add_actions_and_filters();
108 }
109
110 public function add_actions_and_filters() {
111 //CHECKOUT HOOK
112 $checkout_hook = $this->routeapp_get_checkout_hook();
113 add_action($checkout_hook, array($this, 'checkout_route_insurance'), 1, 0);
114
115
116 add_action('woocommerce_checkout_before_order_review', array($this, 'checkout_route_insurance_fee'), 20, 1);
117
118 add_action('woocommerce_cart_calculate_fees', array($this, 'checkout_route_insurance_fee'), 20, 1);
119 add_action('wp_ajax_woo_get_ajax_data', array($this, 'checkout_route_insurance_set_session'));
120 add_action('wp_ajax_nopriv_woo_get_ajax_data', array($this, 'checkout_route_insurance_set_session'));
121
122 add_action('wp_ajax_woo_check_widget', array($this, 'check_route_widget'));
123 add_action('wp_ajax_nopriv_woo_check_widget', array($this, 'check_route_widget'));
124
125 add_action( 'woocommerce_checkout_create_order_line_item', [ $this, 'routeapp_checkout_create_order_line_item' ], 10, 4 );
126 add_action( 'woocommerce_ajax_add_order_item_meta', [$this, 'routeapp_ajax_add_order_item_meta'], 10, 2 );
127 add_filter( 'woocommerce_hidden_order_itemmeta', [$this,'routeapp_woocommerce_hidden_order_itemmeta'], 10, 1);
128
129 add_action( 'added_post_meta', array( $this, 'routeapp_update_tracking_order_api' ), 20, 3 );
130 add_action( 'updated_post_meta', array( $this, 'routeapp_update_tracking_order_api' ), 20, 3 );
131 add_action( 'added_order_item_meta', array( $this, 'routeapp_update_tracking_order_item_api' ), 20, 3 );
132 add_action( 'updated_order_item_meta', array( $this, 'routeapp_update_tracking_order_item_api' ), 20, 3 );
133 add_action('woocommerce_order_status_processing', array($this, 'routeapp_mark_order'), 10, 1);
134 add_action('woocommerce_order_status_completed', array($this, 'routeapp_mark_order'), 8, 2);
135 add_action('woocommerce_new_order', array($this, 'routeapp_save_quote_to_order'), 10, 1);
136 add_action('rest_api_init', array($this, 'routeapp_register_status_route'));
137 add_action('rest_api_init', array($this, 'routeapp_register_setup_route'));
138 add_action('wp_ajax_get_route_checkout', array($this, 'routeapp_register_checkout_route'));
139 add_action('wp_ajax_nopriv_get_route_checkout', array($this, 'routeapp_register_checkout_route'));
140 add_action('wp_head', array($this, 'wc_routeapp_ajaxurl'));
141 add_action('woocommerce_init', array($this, 'register_shortcode'));
142 add_action('woocommerce_rest_prepare_order_note', array($this, 'check_order_note'));
143 add_action('wp_insert_comment', array($this, 'check_order_note_by_note_id'));
144
145 add_action('wp_ajax_routeapp_add_admin_fee', array($this, 'routeapp_add_admin_fee'));
146 add_action('wp_ajax_nopriv_routeapp_add_admin_fee', array($this, 'routeapp_add_admin_fee'));
147 add_action('wp_ajax_routeapp_remove_admin_fee', array($this, 'routeapp_remove_admin_fee'));
148 add_action('wp_ajax_nopriv_routeapp_remove_admin_fee', array($this, 'routeapp_remove_admin_fee'));
149 add_action('wc_avatax_api_fee_line_data', array($this, 'routeapp_edit_avatax_fee'));
150 add_action('woocommerce_shipstation_export_order_xml', array($this, 'routeapp_add_sku_to_route_node') );
151
152 add_action('woocommerce_order_details_after_order_table', array($this, 'order_updates_route_tracking'), 1, 1);
153 }
154
155 /**
156 * Hide order line_item meta_data from Route App
157 *
158 * @param $arr
159 * @return mixed
160 */
161 function routeapp_woocommerce_hidden_order_itemmeta($arr) {
162 $arr[] = '_routeapp_source_product_id';
163 $arr[] = '_routeapp_image_url';
164 $arr[] = '_routeapp_virtual';
165 $arr[] = '_routeapp_weight_value';
166 $arr[] = '_routeapp_weight_unit';
167 $arr[] = '_routeapp_origin_location';
168 return $arr;
169 }
170
171 /**
172 * Fill metadata
173 *
174 * @param $item WC_Order_Item
175 * @param $product WC_Product
176 * @param $entity_type enum('order_item','item')
177 * @return void
178 */
179 public static function fillMetaData($item, $product, $entity_type='order_item') {
180
181 $sourceProductId = $product->get_id();
182 $productImageUrl = (wp_get_attachment_url($product->get_image_id())) ? wp_get_attachment_url($product->get_image_id()) : null;
183 $weightValue = $product->get_weight();
184
185 $originLocation = json_encode([
186 'address' => get_option('woocommerce_store_address'),
187 'address2' => get_option('woocommerce_store_address_2'),
188 'store_city' => get_option('woocommerce_store_city'),
189 'store_postcode' => get_option('woocommerce_store_postcode'),
190 'default_country' => get_option('woocommerce_default_country'),
191 ]);
192
193
194 switch($entity_type) {
195 case 'order_item':
196 //add source_product_id as order line_item meta_data
197 self::addItemMeta($item, '_routeapp_source_product_id', $sourceProductId);
198
199 //add image_url as order line_item meta_data
200 self::addItemMeta($item, '_routeapp_image_url', $productImageUrl);
201
202 //add weight_value as order line_item meta_data
203 self::addItemMeta($item, '_routeapp_weight_value', $weightValue);
204
205 //add weight_unit as order line_item meta_data
206 self::addItemMeta($item, '_routeapp_weight_unit', get_option('woocommerce_weight_unit'));
207
208 //check if product is virtual
209 if ($product->is_virtual()) {
210 //add virtual as order line_item meta_data
211 self::addItemMeta($item, '_routeapp_virtual', 1);
212 }
213 //add origin location as order line_item meta_data
214 self::addItemMeta($item, '_routeapp_origin_location', $originLocation);
215 break;
216 case 'item':
217 //add source_product_id as order line_item meta_data
218 self::addOrderItemMeta($item->get_id(), '_routeapp_source_product_id', $sourceProductId);
219
220 //add image_url as order line_item meta_data
221 self::addOrderItemMeta($item->get_id(), '_routeapp_image_url', $productImageUrl);
222
223 //add weight_value as order line_item meta_data
224 self::addOrderItemMeta($item->get_id(), '_routeapp_weight_value', $weightValue);
225
226 //add weight_unit as order line_item meta_data
227 self::addOrderItemMeta($item->get_id(), '_routeapp_weight_unit', get_option('woocommerce_weight_unit'));
228
229 //check if product is virtual
230 if ($product->is_virtual()) {
231 //add virtual as order line_item meta_data
232 self::addOrderItemMeta($item->get_id(), '_routeapp_virtual', 1);
233 }
234 //add origin location as order line_item meta_data
235 self::addOrderItemMeta($item->get_id(), '_routeapp_origin_location', $originLocation);
236 break;
237 }
238 }
239
240 /**
241 * Add order line_item meta_data on ajax add to cart
242 *
243 * @param $item_id String
244 * @param $item WC_Order_Item
245 * @return false|void
246 */
247 public static function routeapp_ajax_add_order_item_meta( $item_id, $item) {
248 $product = $item->get_product(); // Get the WC_Product object
249 if ($product instanceof WC_Product) {
250 try {
251 self::fillMetaData($item, $product, 'item');
252 }catch (Exception $exception) {
253 return false;
254 }
255 }
256 }
257
258 /**
259 * @param $itemId WC_Order_Item ID
260 * @param $key String
261 * @param $value String
262 * @return bool
263 */
264 private static function addOrderItemMeta($itemId, $key, $value) {
265 try {
266 $fieldExists = wc_get_order_item_meta( $itemId, $key, true);
267 if (!$fieldExists) {
268 wc_add_order_item_meta($itemId, $key, $value);
269 }
270 }catch (Exception $exception) {
271 return false;
272 }
273 return true;
274 }
275
276 /**
277 * @param $item WC_Order_Item
278 * @param $key String
279 * @param $value String
280 * @return bool
281 */
282 private static function addItemMeta($item, $key, $value) {
283 try {
284 $fieldExists = $item->get_meta($key, true);
285 if (!$fieldExists) {
286 $item->add_meta_data( $key, $value );
287 }
288 }catch (Exception $exception) {
289 return false;
290 }
291 return true;
292 }
293
294 /**
295 * Add order line_item meta_data on checkout add to cart
296 *
297 * @param $item WC_Order_Item
298 * @param $cart_item_key String
299 * @param $values Object
300 * @param $order WC_Order
301 * @return false|void
302 */
303 public function routeapp_checkout_create_order_line_item( $item, $cart_item_key, $values, $order ) {
304 if ( $order instanceof WC_Order ) {
305 $product = $item->get_product(); // Get the WC_Product object
306 if ($product instanceof WC_Product) {
307 try {
308 self::fillMetaData($item, $product, 'order_item');
309 }catch (Exception $exception) {
310 return false;
311 }
312 }
313 }
314 }
315
316 public function routeapp_add_admin_fee() {
317 $this->routeapp_admin_fee->routeapp_add_admin_fee();
318 }
319
320 public function routeapp_remove_admin_fee() {
321 $this->routeapp_admin_fee->routeapp_remove_admin_fee();
322 }
323
324 public function register_shortcode() {
325 add_shortcode( 'route', array( $this, 'route_widget_shortcode' ) );
326 }
327
328 public function get_allowed_quote_type() {
329 return self::ALLOWED_QUOTE_TYPE;
330 }
331
332 public function check_route_widget() {
333 if (!isset($_POST) || !$_POST['shipping_method']) {
334 wp_send_json_error('its missing shipping method on the request');
335 return;
336 }
337 $shipping_method = isset($_POST['shipping_method']) ? $_POST['shipping_method'] : '';
338 $response = [];
339 $response['show_widget'] = $this->routeapp_is_shipping_method_allowed($shipping_method);
340 wp_send_json_success( $response );
341 return;
342 }
343
344 /**
345 * set routeapp session
346 *
347 * @since 1.0.0
348 */
349 public function checkout_route_insurance_set_session() {
350 if ( isset($_POST['checkbox']) && $_POST['checkbox'] === 'true' ){
351 WC()->session->set('checkbox_checked', true );
352 } else {
353 WC()->session->set( 'checkbox_checked', false );
354 }
355 }
356
357 /**
358 * Add route insurance fee in checkout and cart
359 * @since 1.0.0
360 * @param Object $cart
361 *
362 */
363 public function checkout_route_insurance_fee( $cart ) {
364 if ( (is_admin() && ! defined( 'DOING_AJAX' )) ) return;
365
366 $cart = empty($cart) ? WC()->cart : $cart;
367 $checkbox = WC()->session->get( 'checkbox_checked' );
368
369 $cartRef = $cart->get_cart_hash();
370 $cartTotal = round($this->get_cart_subtotal_with_only_shippable_items($cart), 2);
371 $currency = get_woocommerce_currency();
372 $cartItems = $this->get_cart_shippable_items($cart);
373 $route_insurance_quote = $this->routeapp_get_quote_from_api($cartRef, $cartTotal, $currency, $cartItems);
374
375 $route_insurance_amount = false;
376 //only add fee if the quote is covered by customer and has a valid amount
377 if (isset($route_insurance_quote->premium->amount) &&
378 (isset($route_insurance_quote->payment_responsible->type)) && $route_insurance_quote->payment_responsible->type==self::ALLOWED_QUOTE_TYPE) {
379 $route_insurance_amount = $route_insurance_quote->premium->amount;
380 }
381 if (is_null($checkbox) && isset($route_insurance_quote->payment_responsible->ToggleState)) {
382 $checkbox = $route_insurance_quote->payment_responsible->ToggleState;
383 WC()->session->set('checkbox_checked', $checkbox);
384 }
385
386 Route_Setup::set_setup_check_widget(Route_Setup::SETUP_CHECK_WIDGET_PHP);
387 if ( (is_null($checkbox) && is_checkout() || ($checkbox === TRUE || $checkbox === 1)) && $route_insurance_amount > 0 && $this->routeapp_is_shipping_method_allowed()) {
388 $cart->add_fee( $this->routeapp_get_insurance_label(),
389 $route_insurance_amount,
390 $this->routeapp_get_fee_taxable(),
391 $this->routeapp_get_taxable_class());
392 }
393 }
394
395 /**
396 * Create checkout
397 * @since 1.0.0
398 *
399 */
400 public function checkout_route_insurance() {
401 echo do_shortcode('[route]');
402 }
403
404 public function order_updates_route_tracking($order) {
405 if ($this->routeapp_route_show_order_updates()) {
406 echo $this->route_show_thankyoupage_asset($order);
407 }
408 }
409
410 public function route_show_thankyoupage_asset($order) {
411 $output = '';
412
413 $site = parse_url(get_site_url(get_current_blog_id()), PHP_URL_HOST);
414 $response = Routeapp_API_Client::getInstance()->asset_settings($site);
415
416 if (is_wp_error($response) ||
417 (isset($response['response']['code']) && $response['response']['code'] == 404) ||
418 is_null($response['body'])) {
419 return $output;
420 } else {
421
422 $body = json_decode($response['body']);
423
424 if (!is_null($body) &&
425 !is_null($body->asset_settings) &&
426 !is_null($body->asset_settings->asset_live) &&
427 $body->asset_settings->asset_live) {
428 $raw_html = $body->asset_settings->asset_content->raw_html;
429 $css = $body->asset_settings->asset_content->css_url;
430 $js = isset($body->asset_settings->asset_content->js_url) ? $body->asset_settings->asset_content->js_url : '';
431 $jsPath = '';
432 if (!empty($js)) {
433 $jsPath = '<script type="text/javascript" src="' . $js . '"></script>';
434 }
435 $output = '<link rel="stylesheet" href="' . $css . '" type="text/css" />' .
436 $jsPath .
437 '<section class=woocommerce-order-updates>' .
438 '<h2 class="woocommerce-order-updates__title">Order Updates</h2>' .
439 '</section>' .
440 '<div><span class="os-order-number" style="display:none;">'.$order->get_id().'</span></div>'.
441 '<div>' .
442 $raw_html .
443 '</div>' .
444 '<div>&nbsp;</div>';
445
446 wp_enqueue_script($this->plugin_name . '-analytics', 'https://cdn.routeapp.io/route-analytics/route-analytics.js', array(), $this->version, false);
447 wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'js/routeapp-analytics.js', array('jquery'), $this->version, false);
448 }
449 }
450
451 return $output;
452 }
453
454 public function route_widget_shortcode()
455 {
456 if(is_admin() || empty($this->routeapp_get_public_token())) return;
457
458 $custom_env = getenv('ROUTEAPP_ENVIRONMENT_ENDPOINT');
459 if (is_null($custom_env) || !$custom_env) {
460 $custom_env = isset($_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT']) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : '';
461 }
462 $custom_env = empty($custom_env) ? 'production' : $custom_env;
463
464 wp_enqueue_script($this->plugin_name . '-widget', 'https://protection-widget.route.com/route-protection-widget.js', array(), $this->version, false);
465
466 wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'js/routeapp-public-pbc.js', array('jquery'), $this->version, false);
467
468 wp_enqueue_style($this->plugin_name . '-widget', plugin_dir_url(__FILE__) . 'css/routeapp-widget.css', array(), $this->version, false);
469
470 global $wp;
471 $parsedUrl = parse_url(home_url( $wp->request));
472
473 switch($custom_env) {
474 case 'stage':
475 $custom_env = 'Route.Environment.Dev';
476 break;
477 default:
478 $custom_env = 'Route.Environment.Production';
479 break;
480 }
481 $checkbox = WC()->session->get( 'checkbox_checked' );
482 $checkbox = $checkbox ? 'Route.Coverage.ActiveByDefault' : 'Route.Coverage.InactiveByDefault';
483
484 $divId = is_checkout() ? 'route-checkout-radio' : 'route-cart-radio';
485
486 $return = '<div id="'.$divId.'">' .
487 '<input type="hidden" value="'. get_woocommerce_currency() .'" class="routeapp-currency">'.
488 '<input type="hidden" value="'. round($this->get_cart_subtotal_with_only_shippable_items(WC()->cart), 2) .'" class="routeapp-subtotal">'.
489 '<input type="hidden" value="' . admin_url('admin-ajax.php') . '" class="routeapp-ajax-url">'.
490 '<input type="hidden" value="' . $parsedUrl['host'] . '" class="routeapp-store-domain">'.
491 '<input type="hidden" value="' . get_option('blogname') . '" class="routeapp-store-name">'.
492 '<input type="hidden" value="' . $this->routeapp_api_client->get_merchant_id() . '" class="routeapp-merchant-id">'.
493 '<input type="hidden" value="' . $custom_env . '" class="routeapp-env">'.
494 '<input type="hidden" value="' . $checkbox .'" class="routeapp-checkbox-cookie">';
495
496 //check if shipping method is allowed
497 $cart = WC()->cart;
498 $shipping_method = $cart->calculate_shipping();
499 if (isset($shipping_method[0]->method_id)) {
500 $shipping_method = $shipping_method[0]->method_id;
501 $show_widget = $this->routeapp_is_shipping_method_allowed($shipping_method);
502 if (!$show_widget) {
503 $return .= '<input type="hidden" value="true" class="routeapp-invalid-shipping-method">';
504 }
505 }
506 return $return . '<div id="RouteWidget" class="route-div"></div></div>' ;
507 }
508
509
510 public function wc_routeapp_ajaxurl() {
511 echo '<script type="text/javascript">' .
512 'var wc_routeapp_ajaxurl = "' . admin_url('admin-ajax.php') . '"' .
513 '</script>';
514 }
515
516 /**
517 * get amount from RouteApp API
518 *
519 * @param $cartRef
520 * @param $cartTotal
521 * @param $currency
522 * @param $cartItems
523 * @return false|mixed
524 */
525 public function routeapp_get_quote_from_api($cartRef, $cartTotal, $currency, $cartItems) {
526 if (!$cartRef) return false;
527 if (!$cartTotal) return false;
528 $currency = !$currency ? get_woocommerce_currency() : $currency;
529
530 $response = $this->routeapp_api_client->get_quote($cartRef, $cartTotal, $currency, $cartItems);
531
532 if (is_wp_error($response)) {
533 return false;
534 } else {
535 if (isset($response['response']['code']) && $response['response']['code'] == 401) {
536 return false;
537 }
538 $price_data = json_decode($response['body']);
539 Route_Setup::set_setup_check_widget(Route_Setup::SETUP_CHECK_WIDGET_JS);
540 return $price_data;
541 }
542 }
543
544 /**
545 * create order data sending for API
546 * @since 1.0.0
547 * @param integer $order_id
548 */
549 public function create_order_data_api($order_id) {
550
551 $order = wc_get_order( $order_id );
552
553 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
554 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
555 // HPOS usage is enabled.
556 $quote = $order->get_meta('_routeapp_quote');
557 } else {
558 // Traditional CPT-based orders are in use.
559 $quote = get_post_meta($order->get_id(), '_routeapp_quote', true);
560 }
561
562 $insurance_selected = false;
563 $insurance_amount = false;
564
565 foreach ($order->get_items('fee') as $fee) {
566 if ($fee->get_name() === $this->routeapp_get_insurance_label()) {
567 $insurance_selected = true;
568 $insurance_amount = $fee->get_amount();
569 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
570 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
571 // HPOS usage is enabled.
572 $order->update_meta_data('_routeapp_route_charge', $fee->get_amount() );
573 $order->save();
574 } else {
575 // Traditional CPT-based orders are in use.
576 update_post_meta( $order->get_id(), '_routeapp_route_charge', $fee->get_amount() );
577 }
578 }
579 }
580 $quoteId = 'Invalid';
581 if ($quote) {
582 $quoteDecoded = json_decode($quote);
583 $quoteId = $quoteDecoded->id;
584 }
585
586 $destination = array(
587 "first_name" => $order->get_billing_first_name(),
588 "last_name" => $order->get_billing_last_name(),
589 "street_address1" => $order->get_shipping_address_1(),
590 "street_address2" => $order->get_shipping_address_2(),
591 "city" => $order->get_shipping_city(),
592 "province" => $order->get_shipping_state(),
593 "zip" => $order->get_shipping_postcode(),
594 "country_code" => $order->get_shipping_country(),
595 );
596 $customerDetails = array(
597 "first_name" => $order->get_billing_first_name(),
598 "last_name" => $order->get_billing_last_name(),
599 "email" => $order->get_billing_email(),
600 "phone" => $order->get_billing_phone()
601 );
602 $order_data = $order->get_data();
603 $is_shipping_method_allowed = $this->routeapp_is_shipping_method_allowed(false, $order);
604
605 $itemdata = array();
606
607 foreach ($order->get_items() as $item) {
608 ## Using WC_Order_Item_Product methods ##
609 $product = $item->get_product(); // Get the WC_Product object
610
611 if ($product instanceof WC_Product) {
612
613 $isProductVirtual = (bool)$product->is_virtual();
614
615 $arrItem = array(
616 "delivery_method" => $isProductVirtual ? 'digital' :
617 ($is_shipping_method_allowed ? 'ship_to_home' : 'ship_to_store'),
618 "is_insured" => $isProductVirtual ? false :
619 ($is_shipping_method_allowed ? true : false),
620 "source_product_id" => $product->get_id(),
621 "source_id" => strval($item->get_id()),
622 "sku" => $product->get_sku(),
623 "name" => $product->get_title(),
624 "price" => $product->get_price(),
625 "quantity" => $item->get_quantity(),
626 "image_url" => (wp_get_attachment_url($product->get_image_id())) ? wp_get_attachment_url($product->get_image_id()) : null,
627 "upc" => ""// WooCommerce doesn’t provide UPC by default
628 );
629
630 array_push($itemdata, $arrItem);
631 }
632
633 }
634
635 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
636 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
637 // HPOS usage is enabled.
638 $order->update_meta_data('_routeapp_route_protection', $insurance_selected );
639 $order->update_meta_data('_routeapp_version', ROUTEAPP_VERSION );
640
641 $order->save();
642 } else {
643 // Traditional CPT-based orders are in use.
644 update_post_meta( $order->get_id(), '_routeapp_route_protection', $insurance_selected );
645 update_post_meta( $order->get_id(), '_routeapp_version', ROUTEAPP_VERSION );
646 }
647
648 $orderData = array(
649 'quote_id' => $quoteId,
650 'source_order_id' => $order->get_id(),
651 'source_order_number' => $order->get_id(),
652 'subtotal' => $order->get_subtotal(),
653 'order_total' => $order->get_total(),
654 'shipping_total' => $order->get_shipping_total(),
655 'discounts_total' => $order->get_discount_total(),
656 'source_created_on' => date(DATE_ATOM, strtotime($order->get_date_created())),
657 'source_updated_on' => date(DATE_ATOM, strtotime($order->get_date_modified())),
658 'taxes' => $order_data['total_tax'],
659 'insurance_selected' => $insurance_selected,
660 'customer_details' => $customerDetails,
661 'shipping_details' => $destination,
662 'line_items' => $itemdata,
663 'currency' => $order->get_currency(),
664 'amount_covered' => $this->get_order_subtotal($order),
665 'origin' => 'woocommerce-module',
666 );
667 //if customer covers the order, pass the paid to insure with Route Fee value
668 if ($insurance_selected && $insurance_amount) {
669 $orderData['paid_to_insure'] = $insurance_amount;
670 }
671
672 return $orderData;
673 }
674
675 /**
676 * Update tracking order data sending for API
677 * @since 1.0.0
678 * @param integer $item_id
679 * @param object $order
680 * @param string $post
681 */
682 public function routeapp_update_tracking_order_api($item_id, $order, $post) {
683 if (is_null($order) || get_post_type($order) !== 'shop_order' || $post == '_edit_lock') {
684 return false;
685 }
686 $this->routeapp_shipment_tracking->update($order, $this);
687 }
688
689 /**
690 * Update tracking order item data sending for API
691 * @since 1.0.0
692 * @param integer $order_id
693 * @param object $post
694 * @param boolean $update
695 */
696 public function routeapp_update_tracking_order_item_api($item_id, $order_item_id, $post) {
697 if (is_null($order_item_id) ||
698 !is_admin() ||
699 $this->_isShippingTrackingCall($post)
700 ) {
701 return false;
702 }
703 $this->routeapp_shipment_tracking->update($order_item_id, $this);
704 }
705
706 /**
707 * Check if the post comes for shipment tracking
708 * @param $post
709 * @return bool
710 */
711 private function _isShippingTrackingCall($post) {
712 $isNotShippingTracking = $post != '_wc_shipment_tracking_items' &&
713 $post != '_bst_tracking_number' &&
714 $post != 'mimo_tracking_number' &&
715 $post != '_aftership_tracking_number' &&
716 $post != 'ywot_tracking_code' &&
717 $post != 'ph_shipment_tracking_shipping_service' &&
718 $post != 'ph_shipment_tracking_ids' &&
719 $post != 'usps_evs_label_details_array' &&
720 $post != '_order_trackno' &&
721 $post != '_order_trackno1' &&
722 $post != '_order_trackno2' &&
723 $post != '_order_trackno3' &&
724 $post != '_order_trackno4' &&
725 $post != '_vi_wot_order_item_tracking_data' &&
726 $post != 'wc_connect_labels';
727
728 return apply_filters('routeapp_meta_key_is_not_shipping_related', $isNotShippingTracking, $post);
729 }
730
731
732 public function routeapp_update_merchant_api( $merchant_id, $merchantData ) {
733 if ( is_null($merchant_id) ) {
734 return false;
735 }
736
737 $response = $this->routeapp_api_client->update_merchant( $merchant_id, $merchantData );
738 if ( is_wp_error($response) ) {
739 return false;
740 }
741
742 return true;
743 }
744
745 /**
746 * Save into order meta_data the quote from the last request made on session
747 *
748 * @param $order_id
749 */
750 public function routeapp_save_quote_to_order($order_id )
751 {
752 if (!$order_id && !WC()->session) {
753 return;
754 }
755 $order = wc_get_order($order_id);
756 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
757 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
758 // HPOS usage is enabled.
759 $quote = $order->get_meta('_routeapp_quote');
760 } else {
761 // Traditional CPT-based orders are in use.
762 $quote = get_post_meta($order->get_id(), '_routeapp_quote', true);
763 }
764 if (!$quote) {
765 $cartRef = $order->get_cart_hash();
766 if (!$cartRef) {
767 //create uniq ID for backend orders
768 $cartRef = uniqid();
769 }
770 $main_key = $this->routeapp_api_client->get_cache_api_session_key().'-'.$cartRef;
771 $key = $main_key.'-latest';
772 if (WC()->session) {
773 $cached = WC()->session->get($key);
774 if (isset($cached) && is_array($cached) && isset($cached['body'])) {
775
776 $acceptedOrderStatuses = get_option('routeapp_included_order_statuses') ? get_option('routeapp_included_order_statuses') : [];
777 $acceptedCanceledStatuses = get_option('routeapp_cancel_order_statuses') ? get_option('routeapp_cancel_order_statuses') : [];
778
779 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
780 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
781 // HPOS usage is enabled.
782 $order->update_meta_data('_routeapp_quote', $cached['body'] );
783
784 //save to order the configuration selected regarding order statuses
785 $order->update_meta_data('_routeapp_included_order_statuses', json_encode($acceptedOrderStatuses));
786 $order->update_meta_data( '_routeapp_cancel_order_statuses', json_encode($acceptedCanceledStatuses));
787
788 $order->save();
789 } else {
790 // Traditional CPT-based orders are in use.
791 update_post_meta( $order->get_id(), '_routeapp_quote', $cached['body'] );
792 //save to order the configuration selected regarding order statuses
793 update_post_meta( $order->get_id(), '_routeapp_included_order_statuses', json_encode($acceptedOrderStatuses));
794 update_post_meta( $order->get_id(), '_routeapp_cancel_order_statuses', json_encode($acceptedCanceledStatuses));
795 }
796
797 //clear cache!
798 WC()->session->__unset($main_key);
799 WC()->session->__unset($key);
800 }
801 }
802 }
803 }
804
805 /**
806 * Mark order on woocommerce thank you hook
807 * @since 1.0.0
808 * @param integer $order_id
809 *
810 */
811 public function routeapp_mark_order($order_id)
812 {
813 if (!$order_id) {
814 return;
815 }
816
817 $order = wc_get_order($order_id);
818
819 //order cancel action will be handled by webhook event
820 if ($order->get_status() === 'cancelled') {
821 return;
822 }
823
824 // Init the setup for readiness report
825 $this->_setup_readiness_report_check();
826
827 if (WC()->session) {
828 WC()->session->__unset('checkbox_checked');
829 }
830 }
831
832 /**
833 * Check order note (look for shipment tracking data on it)
834 * @param $noteId id
835 * @return mixed
836 */
837 public function check_order_note_by_note_id($noteId)
838 {
839 $comment = get_comment($noteId);
840 $order = wc_get_order($comment->comment_post_ID);
841 if ($order && $order->get_id()) {
842 $this->routeapp_shipment_tracking->parse_order_notes($order->get_id(), $this);
843 }
844 }
845
846 /**
847 * Check order note (look for shipment tracking data on it)
848 * @param $response WP_REST_Response
849 * @return mixed
850 */
851 public function check_order_note($response)
852 {
853 $note = $response->get_data();
854 $comment = get_comment($note['id']);
855 $order = wc_get_order($comment->comment_post_ID);
856 if ($order && $order->get_id()) {
857 $this->routeapp_shipment_tracking->parse_order_notes($order->get_id(), $this);
858 }
859 return $response;
860 }
861 /**
862 * Cancel the tracking order sending for API
863 * @since 1.0.0
864 * @param integer $order_id
865 * @param integer $tracking_number
866 * @param array $product_ids
867 */
868
869 public function routeapp_cancel_tracking_order( $order_id, $tracking_number, $product_ids ) {
870 if ( is_null($order_id) ) {
871 return false;
872 }
873 $this->routeapp_shipment_tracking->cancel($order_id, $tracking_number, $product_ids, $this);
874 }
875
876 /**
877 * Create user at Route
878 * @param $data
879 * @return array|bool|mixed|object
880 */
881 public function routeapp_create_user($data){
882 $response = $this->routeapp_api_client->create_user($data);
883
884 $this->routeapp_last_request = $response;
885
886 if ( is_wp_error( $response ) ) {
887 return false;
888 } else {
889 $body = json_decode($response['body']);
890
891 if ($response['response']['code'] != self::HTTP_CREATED_CODE) {
892 return false;
893 }
894
895 return $body;
896 }
897 }
898
899 /**
900 * Create user at Route
901 *
902 * @param $username
903 * @param $password
904 *
905 * @return array|bool|mixed|object
906 */
907 public function routeapp_user_login($username, $password){
908 $response = $this->routeapp_api_client->login_user($username, $password);
909
910 $this->routeapp_last_request = $response;
911
912 if ( is_wp_error( $response ) ) {
913 return false;
914 } else {
915 $body = json_decode($response['body']);
916
917 if ($response['response']['code'] != self::HTTP_SUCCESS_CODE) {
918 return false;
919 }
920
921 return $body;
922 }
923 }
924
925 /**
926 * Creates merchant at Route
927 *
928 * @param $data
929 * @return array|bool|mixed|object
930 */
931 public function routeapp_create_merchant($data){
932
933 $response = $this->routeapp_api_client->create_merchant($data);
934 $this->routeapp_last_request = $response;
935
936 if ( is_wp_error( $response ) ) {
937 return false;
938 } else {
939 $body = json_decode($response['body']);
940
941 if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
942 return false;
943 }
944
945 return $body;
946 }
947 }
948
949 /**
950 * Creates merchant at Route
951 *
952 * @param $data
953 * @return array|bool|mixed|object
954 */
955 public function routeapp_get_merchants(){
956
957 $response = $this->routeapp_api_client->get_merchants();
958 $this->routeapp_last_request = $response;
959
960 if ( is_wp_error( $response ) ) {
961 return false;
962 } else {
963 $body = json_decode($response['body']);
964
965 if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
966 return false;
967 }
968
969 return $body;
970 }
971 }
972
973 /**
974 * Activate user account
975 *
976 * @param $email
977 *
978 * @return array|bool|mixed|object
979 */
980 public function routeapp_activate_account($email){
981
982 $response = $this->routeapp_api_client->activate_account(['email' => $email]);
983 $this->routeapp_last_request = $response;
984
985 if ( is_wp_error( $response ) ) {
986 return false;
987 } else {
988 $body = json_decode($response['body']);
989
990 if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
991 return false;
992 }
993
994 return $body;
995 }
996 }
997
998 public function routeapp_is_shipping_method_allowed($shipping_method=false, $order=false) {
999
1000 if ($shipping_method || $order) {
1001 if ($shipping_method) {
1002 $shipping_method = explode(':', $shipping_method);
1003 $shipping_method = $shipping_method[0];
1004 }
1005 if ($order) {
1006 $shipping_method = @array_shift($order->get_shipping_methods());
1007 if(isset($shipping_method['method_id']))
1008 {
1009 $shipping_method = $shipping_method['method_id'];
1010 } else {
1011 return false;
1012 }
1013 }
1014 $excludedShippingMethods = $this->routeapp_get_excluded_shipping_methods();
1015 if (is_array($excludedShippingMethods) && in_array($shipping_method, $excludedShippingMethods)) {
1016 return false;
1017 }
1018 } else {
1019 if (WC()->session->get('shipping_for_package_0')) {
1020 foreach( WC()->session->get('shipping_for_package_0')['rates'] as $method_id => $rate ){
1021 if( WC()->session->get('chosen_shipping_methods')[0] == $method_id ){
1022 $shippingMethod = explode(':', $rate->id);
1023 $shippingMethod = $shippingMethod[0];
1024 $excludedShippingMethods = $this->routeapp_get_excluded_shipping_methods();
1025 if (in_array($shippingMethod, $excludedShippingMethods)) {
1026 return false;
1027 }
1028 }
1029 }
1030 }
1031 }
1032 return true;
1033 }
1034
1035 public function routeapp_get_insurance_label() {
1036 return __(self::ROUTE_LABEL, 'routeapp');
1037 }
1038
1039 public function routeapp_is_opt_in() {
1040 $merchant = $this->routeapp_api_client->get_merchant();
1041 return isset($merchant->merchant_preferences->opt_in) && $merchant->merchant_preferences->opt_in == true;
1042 }
1043
1044 public function routeapp_get_public_token() {
1045 return get_option('routeapp_public_token');
1046 }
1047
1048 public static function routeapp_get_taxable_class() {
1049 return get_option('routeapp_taxable_class');
1050 }
1051
1052 public static function routeapp_get_checkout_hook() {
1053 return get_option('routeapp_checkout_hook') ? get_option('routeapp_checkout_hook') : self::DEFAULT_CHECKOUT_WEBHOOK;
1054 }
1055
1056 public static function routeapp_get_checkout_hook_options() {
1057 return [
1058 'woocommerce_before_checkout_form' => 'woocommerce_before_checkout_form',
1059 'woocommerce_checkout_before_customer_details' => 'woocommerce_checkout_before_customer_details',
1060 'woocommerce_checkout_billing' => 'woocommerce_checkout_billing',
1061 'woocommerce_before_checkout_billing_form' => 'woocommerce_before_checkout_billing_form',
1062 'woocommerce_after_checkout_billing_form' => 'woocommerce_after_checkout_billing_form',
1063 'woocommerce_before_checkout_registration_form' => 'woocommerce_before_checkout_registration_form',
1064 'woocommerce_after_checkout_registration_form' => 'woocommerce_after_checkout_registration_form',
1065 'woocommerce_checkout_shipping' => 'woocommerce_checkout_shipping',
1066 'woocommerce_before_checkout_shipping_form' => 'woocommerce_before_checkout_shipping_form',
1067 'woocommerce_after_checkout_shipping_form' => 'woocommerce_after_checkout_shipping_form',
1068 'woocommerce_before_order_notes' => 'woocommerce_before_order_notes',
1069 'woocommerce_after_order_notes' => 'woocommerce_after_order_notes',
1070 'woocommerce_checkout_after_customer_details' => 'woocommerce_checkout_after_customer_details',
1071 'woocommerce_checkout_before_order_review_heading' => 'woocommerce_checkout_before_order_review_heading',
1072 'woocommerce_checkout_order_review' => 'woocommerce_checkout_order_review',
1073 'woocommerce_checkout_before_order_review' => 'woocommerce_checkout_before_order_review',
1074 'woocommerce_review_order_before_cart_contents' => 'woocommerce_review_order_before_cart_contents',
1075 'woocommerce_review_order_after_cart_contents' => 'woocommerce_review_order_after_cart_contents',
1076 'woocommerce_review_order_before_shipping' => 'woocommerce_review_order_before_shipping',
1077 'woocommerce_review_order_after_shipping' => 'woocommerce_review_order_after_shipping',
1078 'woocommerce_review_order_before_order_total' => 'woocommerce_review_order_before_order_total',
1079 'woocommerce_review_order_after_order_total' => 'woocommerce_review_order_after_order_total',
1080 'woocommerce_review_order_before_payment' => 'woocommerce_review_order_before_payment',
1081 'woocommerce_review_order_before_submit' => 'woocommerce_review_order_before_submit',
1082 'woocommerce_review_order_after_submit' => 'woocommerce_review_order_after_submit',
1083 'woocommerce_review_order_after_payment' => 'woocommerce_review_order_after_payment',
1084 'woocommerce_checkout_after_order_review' => 'woocommerce_checkout_after_order_review',
1085 'woocommerce_after_checkout_form' => 'woocommerce_after_checkout_form'
1086 ];
1087 }
1088
1089 public static function routeapp_get_fee_taxable() {
1090 if (get_option('routeapp_route_fee_taxable') == 'yes') return true;
1091 return false;
1092 }
1093
1094 public function routeapp_get_excluded_shipping_methods() {
1095 return get_option('routeapp_excluded_shipping_methods') ?
1096 get_option('routeapp_excluded_shipping_methods') :
1097 [];
1098 }
1099
1100 public function routeapp_get_secret_token() {
1101 return get_option('routeapp_secret_token');
1102 }
1103
1104 public function routeapp_route_enabled_extra_columns() {
1105 return get_option('routeapp_route_enable_extra_columns') !== "no";
1106 }
1107
1108 public function routeapp_get_fresh_installed(){
1109 return get_option('routeapp_route_fresh_installed');
1110 }
1111
1112 public function routeapp_route_show_order_updates() {
1113 return get_option('routeapp_route_show_order_updates', 'yes') == 'yes';
1114 }
1115
1116 private static function routeapp_get_failed_registration(){
1117 return get_option('routeapp_failed_registration');
1118 }
1119
1120 /**
1121 * If it's installation has failed at user step creation step
1122 *
1123 * @return bool
1124 */
1125 public function has_user_creation_failed(){
1126 return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_USER;
1127 }
1128
1129 /**
1130 * If it's installation has failed at user step creation step due to conflict
1131 *
1132 * @return bool
1133 */
1134 public function has_user_creation_conflicted(){
1135 return !Route_Setup::has_valid_merchant_tokens() && (self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_USER_DUPLICATED || self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_USER_LOGIN_FAILED);
1136 }
1137
1138 /**
1139 * If it's installation has failed at Merchant creation step
1140 *
1141 * @return bool
1142 */
1143 public function has_merchant_creation_failed(){
1144 return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_MERCHANT && !Route_Setup::has_valid_merchant_tokens();
1145 }
1146 /**
1147 * If it's installation has failed at Merchant creation step
1148 *
1149 * @return bool
1150 */
1151 public function has_merchant_creation_conflicted(){
1152 return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_MERCHANT_DUPLICATED && !Route_Setup::has_valid_merchant_tokens();
1153 }
1154
1155 /**
1156 * If it's installation has failed at Merchant creation step
1157 *
1158 * @return bool
1159 */
1160 public function successful_registration(){
1161 return self::routeapp_get_failed_registration() === Route_Setup::REGISTRATION_FIXED || self::routeapp_get_failed_registration() === Route_Setup::REGISTRATION_STEP_USER_LOGIN_SUCCESS;
1162 }
1163
1164 /**
1165 * Get registration step at setup flow
1166 * @return mixed
1167 */
1168 private static function registration_step(){
1169 return get_option(Route_Setup::REGISTRATION_STEP);
1170 }
1171
1172 /**
1173 * Check if at the merchant create flow it was already redirected
1174 * @return bool
1175 */
1176 public static function was_redirected(){
1177 return self::registration_step() == Route_Setup::REDIRECTED;
1178 }
1179
1180 /**
1181 * Set it as redirected to Route after registration is complete
1182 * @return void
1183 */
1184 public function set_as_redirected(){
1185 update_option(Route_Setup::REGISTRATION_STEP, Route_Setup::REDIRECTED);
1186 }
1187
1188 public function routeapp_register_status_route() {
1189 register_rest_route( 'route', 'status', array(
1190 'methods' => 'GET',
1191 'callback' => function() {
1192 $response = array(
1193 'version' => ROUTEAPP_VERSION,
1194 'public_token' => !empty($this->routeapp_get_public_token()) ?
1195 $this->_encodeToken($this->routeapp_get_public_token()) :
1196 false,
1197 'secret_key' => !empty($this->routeapp_get_secret_token()) ?
1198 $this->_encodeToken($this->routeapp_get_secret_token()) :
1199 false,
1200 'route_insurance_label' => $this->routeapp_get_insurance_label(),
1201 'extra_columns_enabled' => $this->routeapp_route_enabled_extra_columns(),
1202 'failed_registration' => $this->routeapp_get_failed_registration(),
1203 'base_currency' => get_woocommerce_currency(),
1204 'date' => array(
1205 'date' => date('Y-m-d H:i:s'),
1206 'timezone_type' => date('P'),
1207 'timezone' => date_default_timezone_get()
1208 ),
1209 'routeapp_route_fee_taxable' => $this->routeapp_get_fee_taxable(),
1210 'routeapp_taxable_class' => $this->routeapp_get_taxable_class(),
1211 'routeapp_excluded_shipping_methods' => $this->routeapp_get_excluded_shipping_methods(),
1212 'routeapp_route_show_order_updates' => $this->routeapp_route_show_order_updates(),
1213 'routeapp_checkout_hook' => $this->routeapp_get_checkout_hook(),
1214 'routeapp_have_webhook_secret' => $this->_routeapp_have_webhook_secret()
1215 );
1216 return rest_ensure_response( $response );
1217 },
1218 'permission_callback' => '__return_true'
1219 )
1220 );
1221 }
1222
1223 private function _routeapp_have_webhook_secret() {
1224 return get_option('_routeapp_webhooks_secret') ? 'Yes' : 'No';
1225 }
1226
1227 private function _encodeToken($token) {
1228 return substr($token, 0, 5) . '...' . substr($token, -5);
1229 }
1230
1231 public function routeapp_register_checkout_route(){
1232
1233 if (!isset(WC()->session) || !isset($_POST) || !isset($_POST['shipping_method'])) {
1234 return rest_ensure_response( [] );
1235 }
1236
1237 $shipping_method = $_POST['shipping_method'];
1238
1239 //check first if shipping method is allowed
1240 if ($shipping_method) {
1241 $show_widget = $this->routeapp_is_shipping_method_allowed($shipping_method);
1242 if (!$show_widget) {
1243 wp_send_json(['hide-widget' => true]);
1244 return;
1245 }
1246 }
1247
1248 $cart = WC()->cart;
1249 $this->checkout_route_insurance_fee($cart);
1250 $response= [
1251 'routeapp-subtotal' => round($this->get_cart_subtotal_with_only_shippable_items($cart), 2),
1252 ];
1253
1254 wp_send_json( $response );
1255 }
1256
1257 /**
1258 * Add route to recreate User and Merchant Accounts
1259 * if it has failed at the first try
1260 */
1261 public function routeapp_register_setup_route () {
1262 register_rest_route( 'route', 'recreate_user', array(
1263 'methods' => 'GET',
1264 'callback' => [Route_Setup::class, 'retry_user'],
1265 'permission_callback' => '__return_true'
1266 ) );
1267 register_rest_route( 'route', 'recreate_merchant', array(
1268 'methods' => 'GET',
1269 'callback' => [Route_Setup::class, 'retry_merchant'],
1270 'permission_callback' => '__return_true'
1271 ) );
1272 register_rest_route( 'route', 'user_login', array(
1273 'methods' => 'POST',
1274 'callback' => [Route_Setup::class, 'user_login'],
1275 'permission_callback' => '__return_true'
1276 ) );
1277 }
1278
1279 public function routeapp_last_request_has_failed(){
1280 if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
1281 return true;
1282 } else {
1283 if ($this->routeapp_last_request['response']['code'] != self::HTTP_CREATED_CODE && $this->routeapp_last_request['response']['code'] != self::HTTP_SUCCESS_CODE) {
1284 return true;
1285 }
1286
1287 return false;
1288 }
1289 }
1290
1291 public function routeapp_last_request_has_success() {
1292 if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
1293 return false;
1294 }
1295 return $this->routeapp_last_request['response']['code'] === self::HTTP_SUCCESS_CODE;
1296 }
1297
1298 public function routeapp_last_request_has_conflicted(){
1299 if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
1300 return false;
1301 } else {
1302 if ($this->routeapp_last_request['response']['code'] == self::HTTP_CONFLICT_CODE) {
1303 return true;
1304 }
1305
1306 return false;
1307 }
1308 }
1309
1310 public function routeapp_log($event, $extraData = false) {
1311 $this->routeapp_logger->sentry_log($event, $extraData);
1312 }
1313
1314 private function _setup_readiness_report_check()
1315 {
1316 if ($this->route_app_setup_helper->can_send_compatibility_report()) {
1317 /**
1318 * API CALL for incompatibility test
1319 */
1320 $api = new Routeapp_API_Compatibility();
1321 $api->send($this->route_app_setup_helper->prepare_compatibility_data());
1322 }
1323 }
1324
1325 public function get_order_subtotal($order)
1326 {
1327 $subtotal = 0;
1328
1329 if (is_a($order, 'WC_Order')) {
1330 $order_items = $order->get_items();
1331
1332 foreach ($order_items as $order_item_id => $order_item) {
1333 if ($this->isProductVirtual($order_item)) {
1334 //if the product is not found or virtual, skip it
1335 continue;
1336 }
1337 $subtotal += $order_item->get_subtotal();
1338 }
1339 }
1340 return $subtotal;
1341 }
1342
1343 /**
1344 * Check if product inside order item is virtual, return true if product is not valid
1345 * @param $item
1346 * @return bool
1347 */
1348 public function isProductVirtual($item) {
1349 $productId = isset($item['variation_id']) && !empty($item['variation_id']) ?
1350 $item['variation_id'] :
1351 $item['product_id'];
1352
1353 $product = wc_get_product($productId);
1354 if ($product) {
1355 return $product->is_virtual();
1356 }
1357 return true;
1358 }
1359
1360 /**
1361 * Process cart item
1362 *
1363 * @param $cart_item
1364 * @param $allowed_bundle_parents
1365 * @return array
1366 */
1367 public function processCartItem($cart_item, $allowed_bundle_parents) {
1368
1369 $cart_item_subtotal = $cart_item['line_subtotal'];
1370
1371 //support for WooCommerce Product Bundles
1372 if (isset($cart_item['bundled_items'])) {
1373 if ($cart_item['data']->get_data()['price'] > 0) {
1374 //grab item price from parent bundle if exist, return directly
1375 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
1376 $data = [
1377 'id' => strval($cart_item['product_id']),
1378 'quantity' => intval($cart_item['quantity']),
1379 'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2))
1380 ];
1381 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'data' => $data];
1382 } else{
1383 //add bundle parent key to allowed array
1384 $allowed_bundle_parents[] = $cart_item['key'];
1385 }
1386 }
1387
1388 if ($this->isProductVirtual($cart_item)) {
1389 //if the product is not found or virtual, skip it
1390 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1391 }
1392
1393 if (isset($cart_item['bundled_by'])) {
1394 if (!in_array($cart_item['bundled_by'], $allowed_bundle_parents)) {
1395 //skip bundle item children (price is on parent)
1396 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1397 } else {
1398 //grab subtotal from bundle child
1399 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
1400 }
1401 }
1402
1403 return [
1404 'allowed_bundle_parents' => $allowed_bundle_parents,
1405 'data' => [
1406 'id' => strval($cart_item['product_id']),
1407 'quantity' => intval($cart_item['quantity']),
1408 'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2))
1409 ]
1410 ];
1411 }
1412
1413 /**
1414 * Get cart item subtotal
1415 *
1416 * @param $cart_item
1417 * @param $allowed_bundle_parents
1418 * @return array
1419 */
1420 public function getCartItemSubtotal($cart_item, $allowed_bundle_parents) {
1421
1422 $cart_item_subtotal = $cart_item['line_subtotal'];
1423
1424 //support for WooCommerce Product Bundles
1425 if (isset($cart_item['bundled_items'])) {
1426 if ($cart_item['data']->get_data()['price'] > 0) {
1427 //grab item price from parent bundle if exist, return directly
1428 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
1429 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal];
1430 } else{
1431 //add bundle parent key to allowed array
1432 $allowed_bundle_parents[] = $cart_item['key'];
1433 }
1434 }
1435
1436 if ($this->isProductVirtual($cart_item)) {
1437 //if the product is not found or virtual, skip it
1438 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1439 }
1440
1441 if (isset($cart_item['bundled_by'])) {
1442 if (!in_array($cart_item['bundled_by'], $allowed_bundle_parents)) {
1443 //skip bundle item children (price is on parent)
1444 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1445 } else {
1446 //grab subtotal from bundle child
1447 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
1448 }
1449 }
1450 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal];
1451 }
1452
1453 /**
1454 * get cart shippable items to grab quote on RouteApp
1455 *
1456 * @param $cart
1457 * @param string $object
1458 * @return array
1459 */
1460 public function get_cart_shippable_items($cart, $object='cart')
1461 {
1462 $cartItems = [];
1463 if (!$cart) {
1464 return $cartItems;
1465 }
1466 $allowed_bundle_parents = [];
1467 if ($object=='cart') {
1468 foreach ( $cart->get_cart() as $cart_item ) {
1469 $cartItemData = $this->processCartItem($cart_item, $allowed_bundle_parents);
1470 $allowed_bundle_parents = $cartItemData['allowed_bundle_parents'];
1471 if (isset($cartItemData['data'])) {
1472 $cartItems[] = $cartItemData['data'];
1473 }
1474 }
1475 } else {
1476 foreach ( $cart->get_items() as $cart_item ) {
1477 $cartItemData = $this->processCartItem($cart_item, $allowed_bundle_parents);
1478 $allowed_bundle_parents = $cartItemData['allowed_bundle_parents'];
1479 if (isset($cartItemData['data'])) {
1480 $cartItems[] = $cartItemData['data'];
1481 }
1482 }
1483 }
1484
1485 return $cartItems;
1486 }
1487
1488 /**
1489 * get subtotal for shippable items only
1490 *
1491 * @param $cart
1492 * @param string $object
1493 * @return float|int|mixed
1494 */
1495 public function get_cart_subtotal_with_only_shippable_items($cart, $object='cart')
1496 {
1497 $subtotal = 0;
1498 if (!$cart) {
1499 return $subtotal;
1500 }
1501 $allowed_bundle_parents = [];
1502
1503 if ($object=='cart') {
1504 foreach ( $cart->get_cart() as $cart_item ) {
1505 $itemSubtotal = $this->getCartItemSubtotal($cart_item, $allowed_bundle_parents);
1506 $allowed_bundle_parents = $itemSubtotal['allowed_bundle_parents'];
1507 if (isset($itemSubtotal['subtotal'])) {
1508 $subtotal += $itemSubtotal['subtotal'];
1509 }
1510 }
1511 } else {
1512 foreach ( $cart->get_items() as $cart_item ) {
1513 $itemSubtotal = $this->getCartItemSubtotal($cart_item, $allowed_bundle_parents);
1514 $allowed_bundle_parents = $itemSubtotal['allowed_bundle_parents'];
1515 if (isset($itemSubtotal['subtotal'])) {
1516 $subtotal += $itemSubtotal['subtotal'];
1517 }
1518 }
1519 }
1520 return $subtotal;
1521 }
1522
1523 /**
1524 * Edit Avatax fee line (send tax class name instead item_id)
1525 */
1526 public function routeapp_edit_avatax_fee($fee_line, $fee = null)
1527 {
1528 if (isset($fee_line['description']) && $fee_line['description'] == $this->routeapp_get_insurance_label()) {
1529 $fee_line['itemCode'] = $this->routeapp_get_taxable_class();
1530 }
1531
1532 return $fee_line;
1533 }
1534
1535
1536 /**
1537 * This method adds to xml export an SKU tag in order to identify
1538 * Route Protection to ShipStation integration
1539 *
1540 * @param $order_xml
1541 * @return mixed
1542 */
1543 public function routeapp_add_sku_to_route_node($order_xml){
1544
1545 if ($order_xml instanceof DOMElement){
1546
1547 $items = $order_xml->getElementsByTagName('Items');
1548
1549 foreach ($items as $itemNode) {
1550 foreach ($itemNode->getElementsByTagName('Item') as $item) {
1551 foreach ($item->getElementsByTagName('Name') as $name) {
1552 if (trim($name->nodeValue) === self::ROUTE_LABEL)
1553 {
1554 $this->xml_append($item, 'SKU', self::ROUTE_PROTECTION_SKU);
1555 }
1556 }
1557 }
1558 }
1559
1560 }
1561
1562 return $order_xml;
1563 }
1564
1565 /**
1566 * Append XML as cdata
1567 */
1568 private function xml_append( $append_to, $name, $value, $cdata = true ) {
1569 $data = $append_to->appendChild( $append_to->ownerDocument->createElement( $name ) );
1570 if ( $cdata ) {
1571 $data->appendChild( $append_to->ownerDocument->createCDATASection( $value ) );
1572 } else {
1573 $data->appendChild( $append_to->ownerDocument->createTextNode( $value ) );
1574 }
1575 }
1576 }
1577
1578