PluginProbe
Route ‑ Shipping Protection / 2.4.8
Route ‑ Shipping Protection v2.4.8
2.4.19 2.4.18 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 All 153 releases
routeapp / public / class-routeapp-public.php

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

2,178 lines 83.1 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 <[email protected]>
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 const AMPLITUDE_API_KEY = [
39 'production' => '6d4f9158886e0c6d78e3bd97663ba920',
40 'stage' => '99a869d62986f6415610c6741057bc92',
41 'dev' => '527af176070cf4b775c6a718f0737f0a'
42 ];
43
44 /**
45 * The ID of this plugin.
46 *
47 * @since 1.0.0
48 * @access private
49 * @var string $plugin_name The ID of this plugin.
50 */
51 private $plugin_name;
52
53 private $route_insurance_data = null;
54
55 /**
56 * The version of this plugin.
57 *
58 * @since 1.0.0
59 * @access private
60 * @var string $version The current version of this plugin.
61 */
62 private $version;
63
64 /**
65 * Routeapp API instance
66 * @since 1.0.0
67 * @access private
68 * @var string $routeapp_api Routeapp API Client
69 */
70 public $routeapp_api_client;
71
72
73 /**
74 * Route Shipment Tracking Module
75 */
76 private $routeapp_shipment_tracking;
77
78 /**
79 * Last Request
80 */
81 private $routeapp_last_request;
82
83 /**
84 * Route Logger
85 */
86 private $routeapp_logger;
87
88 /**
89 * Route Admin Fee
90 */
91 private $routeapp_admin_fee;
92
93 /**
94 * Route Setup
95 */
96 private $route_app_setup_helper;
97
98 /**
99 * Whether the custom Route checkout block has been rendered on this request.
100 */
101 private $route_block_rendered = false;
102
103 /**
104 * Initialize the class and set its properties.
105 *
106 * @since 1.0.0
107 * @param string $plugin_name The name of the plugin.
108 * @param string $version The version of this plugin.
109 */
110 public function __construct($plugin_name, $version)
111 {
112
113 $this->plugin_name = $plugin_name;
114 $this->version = $version;
115 $this->routeapp_api_client = new Routeapp_API_Client($this->routeapp_get_public_token(), $this->routeapp_get_secret_token());
116 $this->routeapp_shipment_tracking = new Routeapp_Shipment_Tracking();
117 $this->routeapp_logger = new Routeapp_Logger_Sentry();
118 $this->routeapp_admin_fee = new Routeapp_Admin_Route_Fee();
119 $this->route_app_setup_helper = new Route_Setup();
120 Route_Setup::ensure_store_hash();
121 $this->add_actions_and_filters();
122 }
123
124 public function add_actions_and_filters() {
125 //CHECKOUT HOOK
126 $checkout_hook = $this->routeapp_get_checkout_hook();
127 add_action($checkout_hook, array($this, 'checkout_route_insurance'), 1, 0);
128
129
130 add_action('woocommerce_checkout_before_order_review', array($this, 'checkout_route_insurance_fee'), 20, 1);
131
132 add_action('woocommerce_cart_calculate_fees', array($this, 'checkout_route_insurance_fee'), 20, 1);
133 add_action('wp_ajax_woo_get_ajax_data', array($this, 'checkout_route_insurance_set_session'));
134 add_action('wp_ajax_nopriv_woo_get_ajax_data', array($this, 'checkout_route_insurance_set_session'));
135
136 add_action('wp_ajax_woo_check_widget', array($this, 'check_route_widget'));
137 add_action('wp_ajax_nopriv_woo_check_widget', array($this, 'check_route_widget'));
138
139 add_action( 'woocommerce_checkout_create_order_line_item', [ $this, 'routeapp_checkout_create_order_line_item' ], 10, 4 );
140 add_action( 'woocommerce_ajax_add_order_item_meta', [$this, 'routeapp_ajax_add_order_item_meta'], 10, 2 );
141 add_filter( 'woocommerce_hidden_order_itemmeta', [$this,'routeapp_woocommerce_hidden_order_itemmeta'], 10, 1);
142
143 add_action( 'added_post_meta', array( $this, 'routeapp_update_tracking_order_api' ), 20, 3 );
144 add_action( 'updated_post_meta', array( $this, 'routeapp_update_tracking_order_api' ), 20, 3 );
145
146 add_action('woocommerce_after_order_object_save', array($this, 'routeapp_update_tracking_order_api_HPOS'), 20, 2);
147
148 add_action( 'added_order_item_meta', array( $this, 'routeapp_update_tracking_order_item_api' ), 20, 3 );
149 add_action( 'updated_order_item_meta', array( $this, 'routeapp_update_tracking_order_item_api' ), 20, 3 );
150 add_action('woocommerce_order_status_processing', array($this, 'routeapp_mark_order'), 10, 1);
151 add_action('woocommerce_order_status_completed', array($this, 'routeapp_mark_order'), 8, 2);
152 add_action('woocommerce_new_order', array($this, 'routeapp_save_quote_to_order'), 10, 1);
153 add_action('rest_api_init', array($this, 'routeapp_register_status_route'));
154 add_action('rest_api_init', array($this, 'routeapp_register_setup_route'));
155 add_action('wp_ajax_get_route_checkout', array($this, 'routeapp_register_checkout_route'));
156 add_action('wp_ajax_nopriv_get_route_checkout', array($this, 'routeapp_register_checkout_route'));
157 add_action('wp_head', array($this, 'wc_routeapp_ajaxurl'));
158 add_action('woocommerce_init', array($this, 'register_shortcode'));
159 add_action('woocommerce_rest_prepare_order_note', array($this, 'check_order_note'));
160 add_action('wp_insert_comment', array($this, 'check_order_note_by_note_id'));
161
162 add_action('wp_ajax_routeapp_add_admin_fee', array($this, 'routeapp_add_admin_fee'));
163 add_action('wp_ajax_nopriv_routeapp_add_admin_fee', array($this, 'routeapp_add_admin_fee'));
164 add_action('wp_ajax_routeapp_remove_admin_fee', array($this, 'routeapp_remove_admin_fee'));
165 add_action('wp_ajax_nopriv_routeapp_remove_admin_fee', array($this, 'routeapp_remove_admin_fee'));
166 add_action('wc_avatax_api_fee_line_data', array($this, 'routeapp_edit_avatax_fee'));
167 add_action('woocommerce_shipstation_export_order_xml', array($this, 'routeapp_add_sku_to_route_node') );
168
169 add_action('woocommerce_order_details_after_order_table', array($this, 'order_updates_route_tracking'), 1, 1);
170
171 add_action('woocommerce_blocks_checkout_update_order_meta', array($this, 'routeapp_save_order_status_to_order'), 10, 1);
172 add_action('woocommerce_store_api_checkout_update_order_meta', array($this, 'routeapp_save_order_status_to_order'), 10, 1);
173
174 // Register callback for WooCommerce blocks checkout cart updates
175 add_action('woocommerce_blocks_loaded', array($this, 'routeapp_register_blocks_checkout_callback'));
176
177 // Inject Route widget before order summary in checkout block (default placement)
178 add_filter('render_block_woocommerce/checkout-order-summary-block', array($this, 'routeapp_checkout_block_before_order_summary'), 10, 3);
179
180 // Register the Route Protection custom checkout block
181 add_action('init', array($this, 'routeapp_register_checkout_block'));
182
183 // Register IntegrationInterface for WooCommerce Blocks
184 add_action('woocommerce_blocks_checkout_block_registration', array($this, 'routeapp_register_blocks_integration'));
185
186 // Enqueue widget assets early on cart/checkout for stable script order with Blocks SSR.
187 add_action('wp_enqueue_scripts', array($this, 'routeapp_maybe_enqueue_widget_assets'), 20);
188 add_action('woocommerce_blocks_enqueue_checkout_block_scripts_before', array($this, 'routeapp_maybe_enqueue_widget_assets'));
189 }
190
191 /**
192 * Hide order line_item meta_data from Route App
193 *
194 * @param $arr
195 * @return mixed
196 */
197 function routeapp_woocommerce_hidden_order_itemmeta($arr) {
198 $arr[] = '_routeapp_source_product_id';
199 $arr[] = '_routeapp_image_url';
200 $arr[] = '_routeapp_virtual';
201 $arr[] = '_routeapp_weight_value';
202 $arr[] = '_routeapp_weight_unit';
203 $arr[] = '_routeapp_origin_location';
204 return $arr;
205 }
206
207 /**
208 * Fill metadata
209 *
210 * @param $item WC_Order_Item
211 * @param $product WC_Product
212 * @param $entity_type enum('order_item','item')
213 * @return void
214 */
215 public static function fillMetaData($item, $product, $entity_type='order_item') {
216
217 $sourceProductId = $product->get_id();
218 $productImageUrl = (wp_get_attachment_url($product->get_image_id())) ? wp_get_attachment_url($product->get_image_id()) : null;
219 $weightValue = $product->get_weight();
220
221 $originLocation = json_encode([
222 'address' => get_option('woocommerce_store_address'),
223 'address2' => get_option('woocommerce_store_address_2'),
224 'store_city' => get_option('woocommerce_store_city'),
225 'store_postcode' => get_option('woocommerce_store_postcode'),
226 'default_country' => get_option('woocommerce_default_country'),
227 ]);
228
229
230 switch($entity_type) {
231 case 'order_item':
232 //add source_product_id as order line_item meta_data
233 self::addItemMeta($item, '_routeapp_source_product_id', $sourceProductId);
234
235 //add image_url as order line_item meta_data
236 self::addItemMeta($item, '_routeapp_image_url', $productImageUrl);
237
238 //add weight_value as order line_item meta_data
239 self::addItemMeta($item, '_routeapp_weight_value', $weightValue);
240
241 //add weight_unit as order line_item meta_data
242 self::addItemMeta($item, '_routeapp_weight_unit', get_option('woocommerce_weight_unit'));
243
244 //check if product is virtual
245 if ($product->is_virtual()) {
246 //add virtual as order line_item meta_data
247 self::addItemMeta($item, '_routeapp_virtual', 1);
248 }
249 //add origin location as order line_item meta_data
250 self::addItemMeta($item, '_routeapp_origin_location', $originLocation);
251 break;
252 case 'item':
253 //add source_product_id as order line_item meta_data
254 self::addOrderItemMeta($item->get_id(), '_routeapp_source_product_id', $sourceProductId);
255
256 //add image_url as order line_item meta_data
257 self::addOrderItemMeta($item->get_id(), '_routeapp_image_url', $productImageUrl);
258
259 //add weight_value as order line_item meta_data
260 self::addOrderItemMeta($item->get_id(), '_routeapp_weight_value', $weightValue);
261
262 //add weight_unit as order line_item meta_data
263 self::addOrderItemMeta($item->get_id(), '_routeapp_weight_unit', get_option('woocommerce_weight_unit'));
264
265 //check if product is virtual
266 if ($product->is_virtual()) {
267 //add virtual as order line_item meta_data
268 self::addOrderItemMeta($item->get_id(), '_routeapp_virtual', 1);
269 }
270 //add origin location as order line_item meta_data
271 self::addOrderItemMeta($item->get_id(), '_routeapp_origin_location', $originLocation);
272 break;
273 }
274 }
275
276 /**
277 * Add order line_item meta_data on ajax add to cart
278 *
279 * @param $item_id String
280 * @param $item WC_Order_Item
281 * @return false|void
282 */
283 public static function routeapp_ajax_add_order_item_meta( $item_id, $item) {
284 $product = $item->get_product(); // Get the WC_Product object
285 if ($product instanceof WC_Product) {
286 try {
287 self::fillMetaData($item, $product, 'item');
288 }catch (Exception $exception) {
289 return false;
290 }
291 }
292 }
293
294 /**
295 * @param $itemId WC_Order_Item ID
296 * @param $key String
297 * @param $value String
298 * @return bool
299 */
300 private static function addOrderItemMeta($itemId, $key, $value) {
301 try {
302 $fieldExists = wc_get_order_item_meta( $itemId, $key, true);
303 if (!$fieldExists) {
304 wc_add_order_item_meta($itemId, $key, $value);
305 }
306 }catch (Exception $exception) {
307 return false;
308 }
309 return true;
310 }
311
312 /**
313 * @param $item WC_Order_Item
314 * @param $key String
315 * @param $value String
316 * @return bool
317 */
318 private static function addItemMeta($item, $key, $value) {
319 try {
320 $fieldExists = $item->get_meta($key, true);
321 if (!$fieldExists) {
322 $item->add_meta_data( $key, $value );
323 }
324 }catch (Exception $exception) {
325 return false;
326 }
327 return true;
328 }
329
330 /**
331 * Add order line_item meta_data on checkout add to cart
332 *
333 * @param $item WC_Order_Item
334 * @param $cart_item_key String
335 * @param $values Object
336 * @param $order WC_Order
337 * @return false|void
338 */
339 public function routeapp_checkout_create_order_line_item( $item, $cart_item_key, $values, $order ) {
340 if ( $order instanceof WC_Order ) {
341 $product = $item->get_product(); // Get the WC_Product object
342 if ($product instanceof WC_Product) {
343 try {
344 self::fillMetaData($item, $product, 'order_item');
345 }catch (Exception $exception) {
346 return false;
347 }
348 }
349 }
350 }
351
352 public function routeapp_add_admin_fee() {
353 $this->routeapp_admin_fee->routeapp_add_admin_fee();
354 }
355
356 public function routeapp_remove_admin_fee() {
357 $this->routeapp_admin_fee->routeapp_remove_admin_fee();
358 }
359
360 public function register_shortcode() {
361 add_shortcode( 'route', array( $this, 'route_widget_shortcode' ) );
362 }
363
364 public function get_allowed_quote_type() {
365 return self::ALLOWED_QUOTE_TYPE;
366 }
367
368 public function check_route_widget() {
369 if (!isset($_POST) || !$_POST['shipping_method']) {
370 wp_send_json_error('its missing shipping method on the request');
371 return;
372 }
373 $shipping_method = isset($_POST['shipping_method']) ? $_POST['shipping_method'] : '';
374 $response = [];
375 $response['show_widget'] = $this->routeapp_is_shipping_method_allowed($shipping_method);
376 wp_send_json_success( $response );
377 return;
378 }
379
380 /**
381 * set routeapp session
382 *
383 * @since 1.0.0
384 */
385 public function checkout_route_insurance_set_session() {
386 if ( isset($_POST['checkbox']) && $_POST['checkbox'] === 'true' ){
387 WC()->session->set('checkbox_checked', true );
388 } else {
389 WC()->session->set( 'checkbox_checked', false );
390 }
391 }
392
393 /**
394 * Add route insurance fee in checkout and cart
395 * @since 1.0.0
396 * @param Object $cart_or_checkout
397 * MSS-6261:The issue: checkout_route_insurance_fee is bound to hooks that pass different parameter types:
398 * woocommerce_checkout_before_order_review -> WC_Checkout
399 * woocommerce_cart_calculate_fees -> WC_Cart
400 * At line 388, it assumes a WC_Cart and calls get_cart_hash(), which fails when a WC_Checkout is passed.
401 * Fixing by detecting the object type and handling both cases.
402 */
403 public function checkout_route_insurance_fee( $cart_or_checkout ) {
404 if ( (is_admin() && ! defined( 'DOING_AJAX' )) ) return;
405
406 // Determine if we received a WC_Cart or WC_Checkout object
407 $is_checkout_object = is_a( $cart_or_checkout, 'WC_Checkout' );
408 $cart = $is_checkout_object ? WC()->cart : $cart_or_checkout;
409
410 // Use WC()->cart as fallback if not a valid WC_Cart
411 if ( is_null( $cart ) || ! is_a( $cart, 'WC_Cart' ) ) {
412 $cart = WC()->cart;
413 }
414
415 $checkbox = WC()->session->get( 'checkbox_checked' );
416
417 $cartRef = $cart->get_cart_hash();
418 $cartTotal = round($this->get_cart_subtotal_with_only_shippable_items($cart), 2);
419 $currency = get_woocommerce_currency();
420 $cartItems = $this->get_cart_shippable_items($cart);
421 $route_insurance_quote = $this->routeapp_get_quote_from_api($cartRef, $cartTotal, $currency, $cartItems);
422 $this->route_insurance_data = $route_insurance_quote;
423
424 $route_insurance_amount = false;
425 //only add fee if the quote is covered by customer and has a valid amount
426 if (isset($route_insurance_quote->premium->amount) &&
427 (isset($route_insurance_quote->payment_responsible->type)) && $route_insurance_quote->payment_responsible->type==self::ALLOWED_QUOTE_TYPE) {
428 $route_insurance_amount = $route_insurance_quote->premium->amount;
429 }
430
431 if (is_null($checkbox) &&
432 isset($route_insurance_quote->payment_responsible) &&
433 property_exists($route_insurance_quote->payment_responsible, 'ToggleState')) {
434 $checkbox = $route_insurance_quote->payment_responsible->ToggleState;
435 WC()->session->set('checkbox_checked', $checkbox);
436 }
437
438 Route_Setup::set_setup_check_widget(Route_Setup::SETUP_CHECK_WIDGET_PHP);
439 if ( (is_null($checkbox) && is_checkout() || ($checkbox === TRUE || $checkbox === 1)) && $route_insurance_amount > 0 && $this->routeapp_is_shipping_method_allowed()) {
440 $cart->add_fee( $this->routeapp_get_insurance_label(),
441 $route_insurance_amount,
442 $this->routeapp_get_fee_taxable(),
443 $this->routeapp_get_taxable_class());
444 }
445 }
446
447 /**
448 * Create checkout
449 * @since 1.0.0
450 *
451 */
452 public function checkout_route_insurance() {
453 echo do_shortcode('[route]');
454 }
455
456 public function order_updates_route_tracking($order) {
457 if ($this->routeapp_route_show_order_updates()) {
458 echo $this->route_show_thankyoupage_asset($order);
459 }
460 }
461
462 public function route_show_thankyoupage_asset($order) {
463 $output = '';
464
465 $site = parse_url(get_site_url(get_current_blog_id()), PHP_URL_HOST);
466 $response = Routeapp_API_Client::getInstance()->asset_settings($site);
467
468 if (is_wp_error($response) ||
469 (isset($response['response']['code']) && $response['response']['code'] == 404) ||
470 is_null($response['body'])) {
471 return $output;
472 } else {
473
474 $body = json_decode($response['body']);
475
476 if (!is_null($body) &&
477 !is_null($body->asset_settings) &&
478 !is_null($body->asset_settings->asset_live) &&
479 $body->asset_settings->asset_live) {
480 $raw_html = $body->asset_settings->asset_content->raw_html;
481 $css = $body->asset_settings->asset_content->css_url;
482 $js = isset($body->asset_settings->asset_content->js_url) ? $body->asset_settings->asset_content->js_url : '';
483 $jsPath = '';
484 if (!empty($js)) {
485 $jsPath = '<script type="text/javascript" src="' . $js . '"></script>';
486 }
487 $output = '<link rel="stylesheet" href="' . $css . '" type="text/css" />' .
488 $jsPath .
489 '<section class=woocommerce-order-updates>' .
490 '<h2 class="woocommerce-order-updates__title">Order Updates</h2>' .
491 '</section>' .
492 '<div><span class="os-order-number" style="display:none;">'.$order->get_id().'</span></div>'.
493 '<div>' .
494 $raw_html .
495 '</div>' .
496 '<div>&nbsp;</div>';
497
498 wp_enqueue_script($this->plugin_name . '-analytics', 'https://cdn.routeapp.io/route-analytics/route-analytics.js', array(), $this->version, false);
499 wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'js/routeapp-analytics.js', array('jquery'), $this->version, false);
500 }
501 }
502
503 return $output;
504 }
505
506 /**
507 * Enqueue Route widget JS/CSS with correct dependency order.
508 * routeapp-public-pbc.js must load after route-protection-widget.js (Route.Protection).
509 */
510 public function routeapp_enqueue_widget_assets() {
511 if ( is_admin() || empty( $this->routeapp_get_public_token() ) ) {
512 return;
513 }
514 if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
515 return;
516 }
517
518 $custom_env = getenv( 'ROUTEAPP_ENVIRONMENT_ENDPOINT' );
519 if ( is_null( $custom_env ) || ! $custom_env ) {
520 $custom_env = isset( $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] ) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : '';
521 }
522 $custom_env = empty( $custom_env ) ? 'production' : $custom_env;
523 if ( ! isset( self::AMPLITUDE_API_KEY[ $custom_env ] ) ) {
524 $custom_env = 'production';
525 }
526
527 $widget_handle = $this->plugin_name . '-widget';
528 wp_enqueue_script(
529 $widget_handle,
530 'https://protection-widget.route.com/route-protection-widget.js',
531 array(),
532 $this->version,
533 true
534 );
535
536 $amplitude_key = self::AMPLITUDE_API_KEY[ $custom_env ];
537
538 // Load Amplitude SDK (standard version, not snippet)
539 wp_enqueue_script( 'amplitude-js', "https://cdn.amplitude.com/script/$amplitude_key.js", array(), null, true );
540 // Enqueue Amplitude Analytics module (depends on jQuery and Amplitude SDK)
541 wp_enqueue_script(
542 $this->plugin_name . '-amplitude-analytics',
543 plugin_dir_url( __FILE__ ) . 'js/amplitude-analytics.js',
544 array( 'jquery', 'amplitude-js' ),
545 $this->version,
546 true
547 );
548 $pbc_deps = array( 'jquery', $widget_handle, $this->plugin_name . '-amplitude-analytics' );
549 if ( wp_script_is( 'wc-blocks-checkout', 'registered' ) ) {
550 $pbc_deps[] = 'wc-blocks-checkout';
551 }
552
553 // Main script depends on amplitude-analytics when available.
554 wp_enqueue_script(
555 $this->plugin_name,
556 plugin_dir_url( __FILE__ ) . 'js/routeapp-public-pbc.js',
557 $pbc_deps,
558 $this->version,
559 true
560 );
561
562 /**
563 * Calculate total Route fee
564 * Uses the premium amount from route insurance data if available,
565 * otherwise defaults to 0.98 as a fallback fee amount
566 */
567 $totalRouteFee = ( is_object( $this->route_insurance_data ) && isset( $this->route_insurance_data->premium->amount ) )
568 ? (float) $this->route_insurance_data->premium->amount
569 : 0.98;
570
571 // Pass cart data to JavaScript - send to amplitude-analytics.js
572 wp_localize_script(
573 $this->plugin_name . '-amplitude-analytics',
574 'cartData',
575 array(
576 'cartId' => WC()->cart->get_cart_hash(),
577 'amplitudeKey' => $amplitude_key,
578 'widgetId' => uniqid( 'widget-', true ),
579 'subtotal' => round( $this->get_cart_subtotal_with_only_shippable_items( WC()->cart ), 2 ),
580 'totalRouteFee' => $totalRouteFee,
581 'environment' => $custom_env,
582 'currency' => get_woocommerce_currency(),
583 'merchantId' => $this->routeapp_api_client->get_merchant_id(),
584 )
585 );
586
587 wp_enqueue_style(
588 $this->plugin_name . '-widget',
589 plugin_dir_url( __FILE__ ) . 'css/routeapp-widget.css',
590 array(),
591 $this->version,
592 'all'
593 );
594 }
595
596 /**
597 * Enqueue widget assets on cart, checkout, and Blocks checkout script hook.
598 */
599 public function routeapp_maybe_enqueue_widget_assets() {
600 if ( is_admin() || empty( $this->routeapp_get_public_token() ) ) {
601 return;
602 }
603 if ( function_exists( 'is_cart' ) && function_exists( 'is_checkout' ) && ! is_cart() && ! is_checkout() ) {
604 return;
605 }
606 $this->routeapp_enqueue_widget_assets();
607 }
608
609 public function route_widget_shortcode()
610 {
611 if(is_admin() || empty($this->routeapp_get_public_token())) return;
612
613 // Enqueue only outside cart/checkout; wp_enqueue_scripts handles those pages.
614 if ( function_exists( 'is_cart' ) && function_exists( 'is_checkout' ) && ! is_cart() && ! is_checkout() ) {
615 $this->routeapp_enqueue_widget_assets();
616 }
617
618 $custom_env = getenv('ROUTEAPP_ENVIRONMENT_ENDPOINT');
619 if (is_null($custom_env) || !$custom_env) {
620 $custom_env = isset($_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT']) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : '';
621 }
622 $custom_env = empty($custom_env) ? 'production' : $custom_env;
623
624 global $wp;
625 $parsedUrl = parse_url(home_url( $wp->request));
626
627 switch($custom_env) {
628 case 'stage':
629 $custom_env = 'Route.Environment.Dev';
630 break;
631 default:
632 $custom_env = 'Route.Environment.Production';
633 break;
634 }
635 $checkbox = WC()->session->get( 'checkbox_checked' );
636 $checkbox = $checkbox ? 'Route.Coverage.ActiveByDefault' : 'Route.Coverage.InactiveByDefault';
637
638 $divId = is_checkout() ? 'route-checkout-radio' : 'route-cart-radio';
639
640 $return = '<div id="'.$divId.'">' .
641 '<input type="hidden" value="'. get_woocommerce_currency() .'" class="routeapp-currency">'.
642 '<input type="hidden" value="'. round($this->get_cart_subtotal_with_only_shippable_items(WC()->cart), 2) .'" class="routeapp-subtotal">'.
643 '<input type="hidden" value="' . admin_url('admin-ajax.php') . '" class="routeapp-ajax-url">'.
644 '<input type="hidden" value="' . $parsedUrl['host'] . '" class="routeapp-store-domain">'.
645 '<input type="hidden" value="' . get_option('blogname') . '" class="routeapp-store-name">'.
646 '<input type="hidden" value="' . $this->routeapp_api_client->get_merchant_id() . '" class="routeapp-merchant-id">'.
647 '<input type="hidden" value="' . $custom_env . '" class="routeapp-env">'.
648 '<input type="hidden" value="' . $checkbox .'" class="routeapp-checkbox-cookie">';
649
650 //check if shipping method is allowed
651 $cart = WC()->cart;
652 $shipping_method = $cart->calculate_shipping();
653 if (isset($shipping_method[0]->method_id)) {
654 $shipping_method = $shipping_method[0]->method_id;
655 $show_widget = $this->routeapp_is_shipping_method_allowed($shipping_method);
656 if (!$show_widget) {
657 $return .= '<input type="hidden" value="true" class="routeapp-invalid-shipping-method">';
658 }
659 }
660 return $return . '<div id="RouteWidget" class="route-div"></div></div>' ;
661 }
662
663
664 public function wc_routeapp_ajaxurl() {
665 echo '<script type="text/javascript">' .
666 'var wc_routeapp_ajaxurl = "' . admin_url('admin-ajax.php') . '"' .
667 '</script>';
668 }
669
670 /**
671 * get amount from RouteApp API
672 *
673 * @param $cartRef
674 * @param $cartTotal
675 * @param $currency
676 * @param $cartItems
677 * @return false|mixed
678 */
679 public function routeapp_get_quote_from_api($cartRef, $cartTotal, $currency, $cartItems) {
680 if (!$cartRef) return false;
681 if (!$cartTotal) return false;
682 $currency = !$currency ? get_woocommerce_currency() : $currency;
683
684 $response = $this->routeapp_api_client->get_quote($cartRef, $cartTotal, $currency, $cartItems);
685
686 if (is_wp_error($response)) {
687 return false;
688 } else {
689 if (isset($response['response']['code']) && $response['response']['code'] == 401) {
690 return false;
691 }
692 $price_data = json_decode($response['body']);
693 Route_Setup::set_setup_check_widget(Route_Setup::SETUP_CHECK_WIDGET_JS);
694 return $price_data;
695 }
696 }
697
698 /**
699 * create order data sending for API
700 * @since 1.0.0
701 * @param integer $order_id
702 */
703 public function create_order_data_api($order_id) {
704
705 $order = wc_get_order( $order_id );
706
707 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
708 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
709 // HPOS usage is enabled.
710 $quote = $order->get_meta('_routeapp_quote');
711 } else {
712 // Traditional CPT-based orders are in use.
713 $quote = get_post_meta($order->get_id(), '_routeapp_quote', true);
714 }
715
716 $insurance_selected = false;
717 $insurance_amount = false;
718
719 foreach ($order->get_items('fee') as $fee) {
720 if ($fee->get_name() === $this->routeapp_get_insurance_label()) {
721 $insurance_selected = true;
722 $insurance_amount = $fee->get_amount();
723 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
724 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
725 // HPOS usage is enabled.
726 $order->update_meta_data('_routeapp_route_charge', $fee->get_amount() );
727 $order->save();
728 } else {
729 // Traditional CPT-based orders are in use.
730 update_post_meta( $order->get_id(), '_routeapp_route_charge', $fee->get_amount() );
731 }
732 }
733 }
734 $quoteId = 'Invalid';
735 if ($quote) {
736 $quoteDecoded = json_decode($quote);
737 $quoteId = $quoteDecoded->id;
738 }
739
740 $destination = array(
741 "first_name" => $order->get_billing_first_name(),
742 "last_name" => $order->get_billing_last_name(),
743 "street_address1" => $order->get_shipping_address_1(),
744 "street_address2" => $order->get_shipping_address_2(),
745 "city" => $order->get_shipping_city(),
746 "province" => $order->get_shipping_state(),
747 "zip" => $order->get_shipping_postcode(),
748 "country_code" => $order->get_shipping_country(),
749 );
750 $customerDetails = array(
751 "first_name" => $order->get_billing_first_name(),
752 "last_name" => $order->get_billing_last_name(),
753 "email" => $order->get_billing_email(),
754 "phone" => $order->get_billing_phone()
755 );
756 $order_data = $order->get_data();
757 $is_shipping_method_allowed = $this->routeapp_is_shipping_method_allowed(false, $order);
758
759 $itemdata = array();
760
761 foreach ($order->get_items() as $item) {
762 ## Using WC_Order_Item_Product methods ##
763 $product = $item->get_product(); // Get the WC_Product object
764
765 if ($product instanceof WC_Product) {
766
767 $isProductVirtual = (bool)$product->is_virtual();
768
769 $arrItem = array(
770 "delivery_method" => $isProductVirtual ? 'digital' :
771 ($is_shipping_method_allowed ? 'ship_to_home' : 'ship_to_store'),
772 "is_insured" => $isProductVirtual ? false :
773 ($is_shipping_method_allowed ? true : false),
774 "source_product_id" => $product->get_id(),
775 "source_id" => strval($item->get_id()),
776 "sku" => $product->get_sku(),
777 "name" => $product->get_title(),
778 "price" => $product->get_price(),
779 "quantity" => $item->get_quantity(),
780 "image_url" => (wp_get_attachment_url($product->get_image_id())) ? wp_get_attachment_url($product->get_image_id()) : null,
781 "upc" => ""// WooCommerce doesn’t provide UPC by default
782 );
783
784 array_push($itemdata, $arrItem);
785 }
786
787 }
788
789 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
790 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
791 // HPOS usage is enabled.
792 $order->update_meta_data('_routeapp_route_protection', $insurance_selected );
793 $order->update_meta_data('_routeapp_version', ROUTEAPP_VERSION );
794
795 $order->save();
796 } else {
797 // Traditional CPT-based orders are in use.
798 update_post_meta( $order->get_id(), '_routeapp_route_protection', $insurance_selected );
799 update_post_meta( $order->get_id(), '_routeapp_version', ROUTEAPP_VERSION );
800 }
801
802 $orderData = array(
803 'quote_id' => $quoteId,
804 'source_order_id' => $order->get_id(),
805 'source_order_number' => $order->get_id(),
806 'subtotal' => $order->get_subtotal(),
807 'order_total' => $order->get_total(),
808 'shipping_total' => $order->get_shipping_total(),
809 'discounts_total' => $order->get_discount_total(),
810 'source_created_on' => date(DATE_ATOM, strtotime($order->get_date_created())),
811 'source_updated_on' => date(DATE_ATOM, strtotime($order->get_date_modified())),
812 'taxes' => $order_data['total_tax'],
813 'insurance_selected' => $insurance_selected,
814 'customer_details' => $customerDetails,
815 'shipping_details' => $destination,
816 'line_items' => $itemdata,
817 'currency' => $order->get_currency(),
818 'amount_covered' => $this->get_order_subtotal($order),
819 'origin' => 'woocommerce-module',
820 );
821 //if customer covers the order, pass the paid to insure with Route Fee value
822 if ($insurance_selected && $insurance_amount) {
823 $orderData['paid_to_insure'] = $insurance_amount;
824 }
825
826 return $orderData;
827 }
828
829 /**
830 * Update tracking order data sending for API
831 * @since 1.0.0
832 * @param integer $item_id
833 * @param object $order
834 * @param string $post
835 */
836 public function routeapp_update_tracking_order_api($item_id, $order, $post) {
837 if (is_null($order) || get_post_type($order) !== 'shop_order' || $post == '_edit_lock') {
838 return false;
839 }
840 $this->routeapp_shipment_tracking->update($order, $this);
841 }
842
843 /**
844 * Update tracking order data sending when HPOS is enable
845 * @param object $order
846 * @param object $data_store
847 */
848 public function routeapp_update_tracking_order_api_HPOS($order, $data_store) {
849 static $already_executed = false;
850
851 if ($already_executed) {
852 return false; // Prevent recursion
853 }
854
855 $already_executed = true;
856
857 // Ensure the $order object is valid and of type 'shop_order'
858 if (!$order || $order->get_type() !== 'shop_order') {
859 return false;
860 }
861
862 $this->routeapp_shipment_tracking->update($order->get_id(), $this);
863 }
864
865 /**
866 * Update tracking order item data sending for API
867 * @since 1.0.0
868 * @param integer $order_id
869 * @param object $post
870 * @param boolean $update
871 */
872 public function routeapp_update_tracking_order_item_api($item_id, $order_item_id, $post) {
873 if (is_null($order_item_id) ||
874 !is_admin() ||
875 $this->_isShippingTrackingCall($post)
876 ) {
877 return false;
878 }
879 $this->routeapp_shipment_tracking->update($order_item_id, $this);
880 }
881
882 /**
883 * Check if the post comes for shipment tracking
884 * @param $post
885 * @return bool
886 */
887 private function _isShippingTrackingCall($post) {
888 $isNotShippingTracking = $post != '_wc_shipment_tracking_items' &&
889 $post != '_bst_tracking_number' &&
890 $post != 'mimo_tracking_number' &&
891 $post != '_aftership_tracking_number' &&
892 $post != 'ywot_tracking_code' &&
893 $post != 'ph_shipment_tracking_shipping_service' &&
894 $post != 'ph_shipment_tracking_ids' &&
895 $post != 'usps_evs_label_details_array' &&
896 $post != '_order_trackno' &&
897 $post != '_order_trackno1' &&
898 $post != '_order_trackno2' &&
899 $post != '_order_trackno3' &&
900 $post != '_order_trackno4' &&
901 $post != '_vi_wot_order_item_tracking_data' &&
902 $post != 'wc_connect_labels';
903
904 return apply_filters('routeapp_meta_key_is_not_shipping_related', $isNotShippingTracking, $post);
905 }
906
907
908 public function routeapp_update_merchant_api( $merchant_id, $merchantData ) {
909 if ( is_null($merchant_id) ) {
910 return false;
911 }
912
913 $response = $this->routeapp_api_client->update_merchant( $merchant_id, $merchantData );
914 if ( is_wp_error($response) ) {
915 return false;
916 }
917
918 return true;
919 }
920
921
922 /**
923 * Updates Route-related metadata to the order after it is placed in draft status.
924 *
925 * This method is triggered when an order is created in draft status and is responsible for
926 * adding meta fields with the list Route sync status.
927 * This prevents incorrect synchronization of draft orders.
928 *
929 * @param int $order_id The ID of the WooCommerce order.
930 */
931 public function routeapp_save_order_status_to_order( $order_id ) {
932 if ( ! $order_id || ! WC()->session ) {
933 return;
934 }
935
936 try {
937 $order = wc_get_order( $order_id );
938
939 if ( ! $order ) {
940 throw new Exception( "Order with ID {$order_id} could not be retrieved." );
941 }
942 $is_hpos = class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
943 && OrderUtil::custom_orders_table_usage_is_enabled();
944
945 $acceptedOrderStatuses = get_option('routeapp_included_order_statuses') ? get_option('routeapp_included_order_statuses') : [];
946 $acceptedCanceledStatuses = get_option('routeapp_cancel_order_statuses') ? get_option('routeapp_cancel_order_statuses') : [];
947
948 $updated = false;
949 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_included_order_statuses', $acceptedOrderStatuses );
950 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_cancel_order_statuses', $acceptedCanceledStatuses );
951
952 if ($updated && $is_hpos) {
953 $order->save();
954 }
955 } catch ( Exception $e ) {
956 error_log( "An error occurred while updating Route order status meta fields for order ID {$order_id}: " . $e->getMessage() );
957 }
958 }
959
960
961 /**
962 * Save into order meta_data the quote from the last request made on session
963 *
964 * @param $order_id
965 */
966 public function routeapp_save_quote_to_order($order_id )
967 {
968 if (!$order_id && !WC()->session) {
969 return;
970 }
971 $order = wc_get_order($order_id);
972 if (!$order) {
973 return;
974 }
975 //MSS-6248, Support High-Performance Order Storage (HPOS); fall back to legacy order storage when disabled.
976 $is_hpos = class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
977 && OrderUtil::custom_orders_table_usage_is_enabled();
978
979 $updated = false;
980
981 if ($is_hpos) {
982 $quote = $order->get_meta('_routeapp_quote');
983 } else {
984 // Traditional CPT-based orders are in use.
985 $quote = get_post_meta($order->get_id(), '_routeapp_quote', true);
986 }
987
988 if (!$quote) {
989 $cartRef = $order->get_cart_hash();
990 if (!$cartRef) {
991 //create uniq ID for backend orders
992 $cartRef = uniqid();
993 }
994 $main_key = $this->routeapp_api_client->get_cache_api_session_key().'-'.$cartRef;
995 $key = $main_key.'-latest';
996 if (WC()->session) {
997 $cached = WC()->session->get($key);
998 if (isset($cached) && is_array($cached) && isset($cached['body'])) {
999
1000 $fees = $order->get_items('fee');
1001 if (!empty($fees)) {
1002 foreach ($fees as $fee) {
1003 if ($fee->get_name() === $this->routeapp_get_insurance_label()) {
1004 $insurance_amount = $fee->get_amount();
1005 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_charge', $insurance_amount );
1006 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_protection', true );
1007 }
1008 }
1009 } else {
1010 try {
1011 // Check if WC()->cart exists and is accessible
1012 if (class_exists('WC_Cart') && isset(WC()->cart) && WC()->cart instanceof WC_Cart) {
1013 // Fallback to WC()->cart->get_fees() if no fees are present in the order
1014 $cart_fees = WC()->cart->get_fees();
1015 if (!empty($cart_fees)) {
1016 foreach ($cart_fees as $cart_fee) {
1017 if ($cart_fee->name === $this->routeapp_get_insurance_label()) {
1018 $insurance_amount = $cart_fee->amount; // Correct method for cart fee amount
1019 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_charge', $insurance_amount );
1020 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_protection', true );
1021 }
1022 }
1023 }
1024 }
1025 } catch (Exception $e) {
1026 // Log the exception and ensure the application flow continues
1027 error_log("An error occurred while processing cart fees: " . $e->getMessage());
1028 }
1029
1030 }
1031 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_quote', $cached['body'] );
1032
1033 //clear cache!
1034 WC()->session->__unset($main_key);
1035 WC()->session->__unset($key);
1036 }
1037 }
1038 }
1039
1040 //Add orders status meta data
1041 $acceptedOrderStatuses = get_option('routeapp_included_order_statuses') ? get_option('routeapp_included_order_statuses') : [];
1042 $acceptedCanceledStatuses = get_option('routeapp_cancel_order_statuses') ? get_option('routeapp_cancel_order_statuses') : [];
1043
1044 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_included_order_statuses', $acceptedOrderStatuses );
1045 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_cancel_order_statuses', $acceptedCanceledStatuses );
1046
1047 //MSS-6533: Add ship to store flag and excluded shipping methods meta data to the order
1048 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos,'_routeapp_order_ship_to_store', $this->routeapp_resolve_ship_to_store_flag( $order ) );
1049 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_excluded_shipping_methods', $this->routeapp_get_excluded_shipping_methods());
1050
1051 // Save order if there is any update
1052 if ($updated && $is_hpos) {
1053 $order->save();
1054 }
1055 }
1056
1057 /**
1058 * Mark order on woocommerce thank you hook
1059 * @since 1.0.0
1060 * @param integer $order_id
1061 *
1062 */
1063 public function routeapp_mark_order($order_id)
1064 {
1065 if (!$order_id) {
1066 return;
1067 }
1068
1069 $order = wc_get_order($order_id);
1070
1071 //order cancel action will be handled by webhook event
1072 if ($order->get_status() === 'cancelled') {
1073 return;
1074 }
1075
1076 // Init the setup for readiness report
1077 $this->_setup_readiness_report_check();
1078
1079 if (WC()->session) {
1080 WC()->session->__unset('checkbox_checked');
1081 }
1082 }
1083
1084 /**
1085 * Check order note (look for shipment tracking data on it)
1086 * @param $noteId id
1087 * @return mixed
1088 */
1089 public function check_order_note_by_note_id($noteId)
1090 {
1091 $comment = get_comment($noteId);
1092 $order = wc_get_order($comment->comment_post_ID);
1093 if ($order && $order->get_id()) {
1094 $this->routeapp_shipment_tracking->parse_order_notes($order->get_id(), $this);
1095 }
1096 }
1097
1098 /**
1099 * Check order note (look for shipment tracking data on it)
1100 * @param $response WP_REST_Response
1101 * @return mixed
1102 */
1103 public function check_order_note($response)
1104 {
1105 $note = $response->get_data();
1106 $comment = get_comment($note['id']);
1107 $order = wc_get_order($comment->comment_post_ID);
1108 if ($order && $order->get_id()) {
1109 $this->routeapp_shipment_tracking->parse_order_notes($order->get_id(), $this);
1110 }
1111 return $response;
1112 }
1113 /**
1114 * Cancel the tracking order sending for API
1115 * @since 1.0.0
1116 * @param integer $order_id
1117 * @param integer $tracking_number
1118 * @param array $product_ids
1119 */
1120
1121 public function routeapp_cancel_tracking_order( $order_id, $tracking_number, $product_ids ) {
1122 if ( is_null($order_id) ) {
1123 return false;
1124 }
1125 $this->routeapp_shipment_tracking->cancel($order_id, $tracking_number, $product_ids, $this);
1126 }
1127
1128 /**
1129 * Create user at Route
1130 * @param $data
1131 * @return array|bool|mixed|object
1132 */
1133 public function routeapp_create_user($data){
1134 $response = $this->routeapp_api_client->create_user($data);
1135
1136 $this->routeapp_last_request = $response;
1137
1138 if ( is_wp_error( $response ) ) {
1139 return false;
1140 } else {
1141 $body = json_decode($response['body']);
1142
1143 if ($response['response']['code'] != self::HTTP_CREATED_CODE) {
1144 return false;
1145 }
1146
1147 return $body;
1148 }
1149 }
1150
1151 /**
1152 * Create user at Route
1153 *
1154 * @param $username
1155 * @param $password
1156 *
1157 * @return array|bool|mixed|object
1158 */
1159 public function routeapp_user_login($username, $password){
1160 $response = $this->routeapp_api_client->login_user($username, $password);
1161
1162 $this->routeapp_last_request = $response;
1163
1164 if ( is_wp_error( $response ) ) {
1165 return false;
1166 } else {
1167 $body = json_decode($response['body']);
1168
1169 if ($response['response']['code'] != self::HTTP_SUCCESS_CODE) {
1170 return false;
1171 }
1172
1173 return $body;
1174 }
1175 }
1176
1177 /**
1178 * Creates merchant at Route
1179 *
1180 * @param $data
1181 * @return array|bool|mixed|object
1182 */
1183 public function routeapp_create_merchant($data){
1184
1185 $response = $this->routeapp_api_client->create_merchant($data);
1186 $this->routeapp_last_request = $response;
1187
1188 if ( is_wp_error( $response ) ) {
1189 return false;
1190 } else {
1191 $body = json_decode($response['body']);
1192
1193 if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
1194 return false;
1195 }
1196
1197 return $body;
1198 }
1199 }
1200
1201 /**
1202 * Creates merchant at Route
1203 *
1204 * @param $data
1205 * @return array|bool|mixed|object
1206 */
1207 public function routeapp_get_merchants(){
1208
1209 $response = $this->routeapp_api_client->get_merchants();
1210 $this->routeapp_last_request = $response;
1211
1212 if ( is_wp_error( $response ) ) {
1213 return false;
1214 } else {
1215 $body = json_decode($response['body']);
1216
1217 if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
1218 return false;
1219 }
1220
1221 return $body;
1222 }
1223 }
1224
1225 /**
1226 * Activate user account
1227 *
1228 * @param $email
1229 *
1230 * @return array|bool|mixed|object
1231 */
1232 public function routeapp_activate_account($email){
1233
1234 $response = $this->routeapp_api_client->activate_account(['email' => $email]);
1235 $this->routeapp_last_request = $response;
1236
1237 if ( is_wp_error( $response ) ) {
1238 return false;
1239 } else {
1240 $body = json_decode($response['body']);
1241
1242 if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
1243 return false;
1244 }
1245
1246 return $body;
1247 }
1248 }
1249
1250 public function routeapp_is_shipping_method_allowed($shipping_method=false, $order=false) {
1251
1252 if ($shipping_method || $order) {
1253 if ($shipping_method) {
1254 $shipping_method = explode(':', $shipping_method);
1255 $shipping_method = $shipping_method[0];
1256 }
1257 if ($order) {
1258 $shipping_method = @array_shift($order->get_shipping_methods());
1259 if(isset($shipping_method['method_id']))
1260 {
1261 $shipping_method = $shipping_method['method_id'];
1262 } else {
1263 return false;
1264 }
1265 }
1266 $excludedShippingMethods = $this->routeapp_get_excluded_shipping_methods();
1267 if (is_array($excludedShippingMethods) && in_array($shipping_method, $excludedShippingMethods)) {
1268 return false;
1269 }
1270 } else {
1271 if (WC()->session->get('shipping_for_package_0')) {
1272 foreach( WC()->session->get('shipping_for_package_0')['rates'] as $method_id => $rate ){
1273 if( WC()->session->get('chosen_shipping_methods')[0] == $method_id ){
1274 $shippingMethod = explode(':', $rate->id);
1275 $shippingMethod = $shippingMethod[0];
1276 $excludedShippingMethods = $this->routeapp_get_excluded_shipping_methods();
1277 if (in_array($shippingMethod, $excludedShippingMethods)) {
1278 return false;
1279 }
1280 }
1281 }
1282 }
1283 }
1284 return true;
1285 }
1286
1287 public function routeapp_get_insurance_label() {
1288 return __(self::ROUTE_LABEL, 'routeapp');
1289 }
1290
1291 public function routeapp_is_opt_in() {
1292 $merchant = $this->routeapp_api_client->get_merchant();
1293 return isset($merchant->merchant_preferences->opt_in) && $merchant->merchant_preferences->opt_in == true;
1294 }
1295
1296 public function routeapp_get_public_token() {
1297 return get_option('routeapp_public_token');
1298 }
1299
1300 public static function routeapp_get_taxable_class() {
1301 return get_option('routeapp_taxable_class');
1302 }
1303
1304 public static function routeapp_get_checkout_hook() {
1305 return get_option('routeapp_checkout_hook') ? get_option('routeapp_checkout_hook') : self::DEFAULT_CHECKOUT_WEBHOOK;
1306 }
1307
1308 public static function routeapp_get_checkout_hook_options() {
1309 return [
1310 'woocommerce_before_checkout_form' => 'woocommerce_before_checkout_form',
1311 'woocommerce_checkout_before_customer_details' => 'woocommerce_checkout_before_customer_details',
1312 'woocommerce_checkout_billing' => 'woocommerce_checkout_billing',
1313 'woocommerce_before_checkout_billing_form' => 'woocommerce_before_checkout_billing_form',
1314 'woocommerce_after_checkout_billing_form' => 'woocommerce_after_checkout_billing_form',
1315 'woocommerce_before_checkout_registration_form' => 'woocommerce_before_checkout_registration_form',
1316 'woocommerce_after_checkout_registration_form' => 'woocommerce_after_checkout_registration_form',
1317 'woocommerce_checkout_shipping' => 'woocommerce_checkout_shipping',
1318 'woocommerce_before_checkout_shipping_form' => 'woocommerce_before_checkout_shipping_form',
1319 'woocommerce_after_checkout_shipping_form' => 'woocommerce_after_checkout_shipping_form',
1320 'woocommerce_before_order_notes' => 'woocommerce_before_order_notes',
1321 'woocommerce_after_order_notes' => 'woocommerce_after_order_notes',
1322 'woocommerce_checkout_after_customer_details' => 'woocommerce_checkout_after_customer_details',
1323 'woocommerce_checkout_before_order_review_heading' => 'woocommerce_checkout_before_order_review_heading',
1324 'woocommerce_checkout_order_review' => 'woocommerce_checkout_order_review',
1325 'woocommerce_checkout_before_order_review' => 'woocommerce_checkout_before_order_review',
1326 'woocommerce_review_order_before_cart_contents' => 'woocommerce_review_order_before_cart_contents',
1327 'woocommerce_review_order_after_cart_contents' => 'woocommerce_review_order_after_cart_contents',
1328 'woocommerce_review_order_before_shipping' => 'woocommerce_review_order_before_shipping',
1329 'woocommerce_review_order_after_shipping' => 'woocommerce_review_order_after_shipping',
1330 'woocommerce_review_order_before_order_total' => 'woocommerce_review_order_before_order_total',
1331 'woocommerce_review_order_after_order_total' => 'woocommerce_review_order_after_order_total',
1332 'woocommerce_review_order_before_payment' => 'woocommerce_review_order_before_payment',
1333 'woocommerce_review_order_before_submit' => 'woocommerce_review_order_before_submit',
1334 'woocommerce_review_order_after_submit' => 'woocommerce_review_order_after_submit',
1335 'woocommerce_review_order_after_payment' => 'woocommerce_review_order_after_payment',
1336 'woocommerce_checkout_after_order_review' => 'woocommerce_checkout_after_order_review',
1337 'woocommerce_after_checkout_form' => 'woocommerce_after_checkout_form'
1338 ];
1339 }
1340
1341 public static function routeapp_get_fee_taxable() {
1342 if (get_option('routeapp_route_fee_taxable') == 'yes') return true;
1343 return false;
1344 }
1345
1346 public function routeapp_get_excluded_shipping_methods() {
1347 return get_option('routeapp_excluded_shipping_methods') ?
1348 get_option('routeapp_excluded_shipping_methods') :
1349 [];
1350 }
1351
1352 public function routeapp_get_secret_token() {
1353 return get_option('routeapp_secret_token');
1354 }
1355
1356 public function routeapp_route_enabled_extra_columns() {
1357 return get_option('routeapp_route_enable_extra_columns') !== "no";
1358 }
1359
1360 public function routeapp_get_fresh_installed(){
1361 return get_option('routeapp_route_fresh_installed');
1362 }
1363
1364 public function routeapp_route_show_order_updates() {
1365 return get_option('routeapp_route_show_order_updates', 'yes') == 'yes';
1366 }
1367
1368 private static function routeapp_get_failed_registration(){
1369 return get_option('routeapp_failed_registration');
1370 }
1371
1372 /**
1373 * If it's installation has failed at user step creation step
1374 *
1375 * @return bool
1376 */
1377 public function has_user_creation_failed(){
1378 return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_USER;
1379 }
1380
1381 /**
1382 * If it's installation has failed at user step creation step due to conflict
1383 *
1384 * @return bool
1385 */
1386 public function has_user_creation_conflicted(){
1387 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);
1388 }
1389
1390 /**
1391 * If it's installation has failed at Merchant creation step
1392 *
1393 * @return bool
1394 */
1395 public function has_merchant_creation_failed(){
1396 return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_MERCHANT && !Route_Setup::has_valid_merchant_tokens();
1397 }
1398 /**
1399 * If it's installation has failed at Merchant creation step
1400 *
1401 * @return bool
1402 */
1403 public function has_merchant_creation_conflicted(){
1404 return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_MERCHANT_DUPLICATED && !Route_Setup::has_valid_merchant_tokens();
1405 }
1406
1407 /**
1408 * If it's installation has failed at Merchant creation step
1409 *
1410 * @return bool
1411 */
1412 public function successful_registration(){
1413 return self::routeapp_get_failed_registration() === Route_Setup::REGISTRATION_FIXED || self::routeapp_get_failed_registration() === Route_Setup::REGISTRATION_STEP_USER_LOGIN_SUCCESS;
1414 }
1415
1416 /**
1417 * Get registration step at setup flow
1418 * @return mixed
1419 */
1420 private static function registration_step(){
1421 return get_option(Route_Setup::REGISTRATION_STEP);
1422 }
1423
1424 /**
1425 * Check if at the merchant create flow it was already redirected
1426 * @return bool
1427 */
1428 public static function was_redirected(){
1429 return self::registration_step() == Route_Setup::REDIRECTED;
1430 }
1431
1432 /**
1433 * Set it as redirected to Route after registration is complete
1434 * @return void
1435 */
1436 public function set_as_redirected(){
1437 update_option(Route_Setup::REGISTRATION_STEP, Route_Setup::REDIRECTED);
1438 }
1439
1440 public function routeapp_register_status_route() {
1441 register_rest_route( 'route', 'status', array(
1442 'methods' => 'GET',
1443 'callback' => function() {
1444 return rest_ensure_response( $this->routeapp_build_status_response() );
1445 },
1446 'permission_callback' => '__return_true'
1447 )
1448 );
1449 }
1450
1451 /**
1452 * Build the associative array returned by GET /wp-json/route/status.
1453 *
1454 * Kept separate from the REST callback so tests can exercise the response
1455 * shape directly without REST dispatch. Never returns raw credentials —
1456 * `routeapp_user_token` and `routeapp_user_id` are intentionally excluded;
1457 * `public_token`, `secret_key`, and `routeapp_store_hash` are masked via
1458 * {@see Routeapp_Public::_encodeToken()}. `routeapp_merchant_id` is
1459 * exposed raw — it is the merchant identifier Route support matches
1460 * against, and already appears in outbound webhook URLs.
1461 *
1462 * @return array Flat associative array keyed by setting name.
1463 */
1464 private function routeapp_build_status_response() {
1465 $store_hash = Route_Setup::get_store_hash();
1466 $widget_install_ts = get_option( Route_Setup::SETUP_CHECK_WIDGET_INSTALLATION_DATE, null );
1467 $widget_install_human = null;
1468 if ( ! empty( $widget_install_ts ) && is_numeric( $widget_install_ts ) ) {
1469 $widget_install_ts = (int) $widget_install_ts;
1470 $widget_install_human = wp_date( 'Y-m-d H:i:s', $widget_install_ts );
1471 } else {
1472 $widget_install_ts = null;
1473 }
1474
1475 return array(
1476 'version' => ROUTEAPP_VERSION,
1477 'public_token' => !empty($this->routeapp_get_public_token()) ?
1478 $this->_encodeToken($this->routeapp_get_public_token()) :
1479 false,
1480 'secret_key' => !empty($this->routeapp_get_secret_token()) ?
1481 $this->_encodeToken($this->routeapp_get_secret_token()) :
1482 false,
1483 'route_insurance_label' => $this->routeapp_get_insurance_label(),
1484 'extra_columns_enabled' => $this->routeapp_route_enabled_extra_columns(),
1485 'failed_registration' => $this->routeapp_get_failed_registration(),
1486 'base_currency' => get_woocommerce_currency(),
1487 'date' => array(
1488 'date' => date('Y-m-d H:i:s'),
1489 'timezone_type' => date('P'),
1490 'timezone' => date_default_timezone_get()
1491 ),
1492 'routeapp_route_fee_taxable' => $this->routeapp_get_fee_taxable(),
1493 'routeapp_taxable_class' => $this->routeapp_get_taxable_class(),
1494 'routeapp_excluded_shipping_methods' => $this->routeapp_get_excluded_shipping_methods(),
1495 'routeapp_route_show_order_updates' => $this->routeapp_route_show_order_updates(),
1496 'routeapp_checkout_hook' => $this->routeapp_get_checkout_hook(),
1497 'routeapp_have_webhook_secret' => $this->_routeapp_have_webhook_secret(),
1498 'routeapp_included_order_statuses' => get_option( 'routeapp_included_order_statuses', array() ),
1499 'routeapp_cancel_order_statuses' => get_option( 'routeapp_cancel_order_statuses', array() ),
1500 'routeapp_external_tracking_providers' => get_option( 'routeapp_external_tracking_providers', '' ),
1501 'routeapp_order_recover_from' => get_option( 'routeapp_order_recover_from', '' ),
1502 'routeapp_order_recover_to' => get_option( 'routeapp_order_recover_to', '' ),
1503 'routeapp_order_recover_reconcile_backend' => get_option( 'routeapp_order_recover_reconcile_backend', 'no' ),
1504 'routeapp_merchant_id' => get_option( Routeapp_API_Client::ROUTEAPP_MERCHANT_ID, '' ),
1505 'routeapp_store_hash' => ! empty( $store_hash ) ? $this->_encodeToken( $store_hash ) : false,
1506 'routeapp_last_install_date' => get_option( '_routeapp_last_install_date', '' ),
1507 'routeapp_webhooks_created' => (bool) get_option( '_routeapp_webhooks_created', false ),
1508 'routeapp_route_registration_step' => get_option( Route_Setup::REGISTRATION_STEP, '' ),
1509 'routeapp_user_conflict_auto_redirect_done' => (bool) get_option( Route_Setup::USER_CONFLICT_AUTO_REDIRECT_DONE_OPTION, false ),
1510 'routeapp_route_fresh_installed' => $this->routeapp_get_fresh_installed(),
1511 'route_setup_check_widget_installation_date' => $widget_install_ts,
1512 'route_setup_check_widget_installation_date_human' => $widget_install_human,
1513 'route_setup_check_widget_php' => (bool) get_option( Route_Setup::SETUP_CHECK_WIDGET_PHP, false ),
1514 'route_setup_check_widget_js' => (bool) get_option( Route_Setup::SETUP_CHECK_WIDGET_JS, false ),
1515 'route_setup_check_widget_api_call' => (bool) get_option( Route_Setup::SETUP_CHECK_WIDGET_API_CALL, false ),
1516 );
1517 }
1518
1519 private function _routeapp_have_webhook_secret() {
1520 return get_option('_routeapp_webhooks_secret') ? 'Yes' : 'No';
1521 }
1522
1523 private function _encodeToken($token) {
1524 return substr($token, 0, 5) . '...' . substr($token, -5);
1525 }
1526
1527 public function routeapp_register_checkout_route(){
1528
1529 if (!isset(WC()->session) || !isset($_POST) || !isset($_POST['shipping_method'])) {
1530 return rest_ensure_response( [] );
1531 }
1532
1533 $shipping_method = $_POST['shipping_method'];
1534
1535 //check first if shipping method is allowed
1536 if ($shipping_method) {
1537 $show_widget = $this->routeapp_is_shipping_method_allowed($shipping_method);
1538 if (!$show_widget) {
1539 wp_send_json(['hide-widget' => true]);
1540 return;
1541 }
1542 }
1543
1544 $cart = WC()->cart;
1545 $this->checkout_route_insurance_fee($cart);
1546 $response= [
1547 'routeapp-subtotal' => round($this->get_cart_subtotal_with_only_shippable_items($cart), 2),
1548 ];
1549
1550 wp_send_json( $response );
1551 }
1552
1553 /**
1554 * Add route to recreate User and Merchant Accounts
1555 * if it has failed at the first try
1556 */
1557 public function routeapp_register_setup_route () {
1558 register_rest_route( 'route', 'recreate_user', array(
1559 'methods' => 'GET',
1560 'callback' => [Route_Setup::class, 'retry_user'],
1561 'permission_callback' => '__return_true'
1562 ) );
1563 register_rest_route( 'route', 'recreate_merchant', array(
1564 'methods' => 'GET',
1565 'callback' => [Route_Setup::class, 'retry_merchant'],
1566 'permission_callback' => '__return_true'
1567 ) );
1568 register_rest_route( 'route', 'user_login', array(
1569 'methods' => 'POST',
1570 'callback' => [Route_Setup::class, 'user_login'],
1571 'permission_callback' => '__return_true'
1572 ) );
1573 }
1574
1575 public function routeapp_last_request_has_failed(){
1576 if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
1577 return true;
1578 } else {
1579 if ($this->routeapp_last_request['response']['code'] != self::HTTP_CREATED_CODE && $this->routeapp_last_request['response']['code'] != self::HTTP_SUCCESS_CODE) {
1580 return true;
1581 }
1582
1583 return false;
1584 }
1585 }
1586
1587 public function routeapp_last_request_has_success() {
1588 if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
1589 return false;
1590 }
1591 return $this->routeapp_last_request['response']['code'] === self::HTTP_SUCCESS_CODE;
1592 }
1593
1594 public function routeapp_last_request_has_conflicted(){
1595 if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
1596 return false;
1597 } else {
1598 if ($this->routeapp_last_request['response']['code'] == self::HTTP_CONFLICT_CODE) {
1599 return true;
1600 }
1601
1602 return false;
1603 }
1604 }
1605
1606 public function routeapp_log($event, $extraData = false) {
1607 $this->routeapp_logger->sentry_log($event, $extraData);
1608 }
1609
1610 private function _setup_readiness_report_check()
1611 {
1612 if ($this->route_app_setup_helper->can_send_compatibility_report()) {
1613 /**
1614 * API CALL for incompatibility test
1615 */
1616 $api = new Routeapp_API_Compatibility();
1617 $api->send($this->route_app_setup_helper->prepare_compatibility_data());
1618 }
1619 }
1620
1621 public function get_order_subtotal($order)
1622 {
1623 $subtotal = 0;
1624
1625 if (is_a($order, 'WC_Order')) {
1626 $order_items = $order->get_items();
1627
1628 foreach ($order_items as $order_item_id => $order_item) {
1629 if ($this->isProductVirtual($order_item)) {
1630 //if the product is not found or virtual, skip it
1631 continue;
1632 }
1633 $subtotal += $order_item->get_subtotal();
1634 }
1635 }
1636 return $subtotal;
1637 }
1638
1639 /**
1640 * Normalize cart item (array) or order item (WC_Order_Item_Product) to a common array format.
1641 * Ensures compatibility with WooCommerce Product Bundles when used in order context.
1642 *
1643 * @param array|WC_Order_Item_Product $item Cart item array or order item object.
1644 * @return array Normalized item with keys: data, line_subtotal, quantity, product_id, variation_id, key, bundled_items, bundled_by.
1645 */
1646 private function normalize_item_for_bundle_calc($item) {
1647 if (is_object($item) && method_exists($item, 'get_product')) {
1648 /** @var WC_Order_Item_Product $item */
1649 $product = $item->get_product();
1650 $bundled_items = $item->get_meta('_bundled_items', true);
1651 $bundled_by = $item->get_meta('_bundled_by', true);
1652 // Use _bundle_cart_key so parent matches child's _bundled_by (Product Bundles uses same hash).
1653 $key = $item->get_meta('_bundle_cart_key', true) ?: $item->get_id();
1654 return [
1655 'data' => $product,
1656 'line_subtotal' => (float) $item->get_subtotal(),
1657 'quantity' => (int) $item->get_quantity(),
1658 'product_id' => $item->get_product_id(),
1659 'variation_id' => $item->get_variation_id(),
1660 'key' => $key,
1661 'bundled_items' => !empty($bundled_items) ? $bundled_items : null,
1662 'bundled_by' => !empty($bundled_by) ? $bundled_by : null,
1663 ];
1664 }
1665 return $item;
1666 }
1667
1668 /**
1669 * Check if product inside order item is virtual, return true if product is not valid
1670 * @param array|WC_Order_Item_Product $item Cart item array or order item object.
1671 * @return bool
1672 */
1673 public function isProductVirtual($item) {
1674 if (is_object($item) && method_exists($item, 'get_product_id')) {
1675 $productId = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id();
1676 } else {
1677 $productId = isset($item['variation_id']) && !empty($item['variation_id']) ?
1678 $item['variation_id'] :
1679 $item['product_id'];
1680 }
1681
1682 $product = wc_get_product($productId);
1683 if ($product) {
1684 return $product->is_virtual();
1685 }
1686 return true;
1687 }
1688
1689 /**
1690 * Process cart item
1691 *
1692 * @param $cart_item
1693 * @param $allowed_bundle_parents
1694 * @return array
1695 */
1696 public function processCartItem($cart_item, $allowed_bundle_parents) {
1697
1698 $cart_item = $this->normalize_item_for_bundle_calc($cart_item);
1699
1700 $cart_item_subtotal = $cart_item['line_subtotal'];
1701
1702 //support for WooCommerce Product Bundles (only when product data is available; order items may have null)
1703 if (isset($cart_item['bundled_items'])) {
1704 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
1705 $price = $cart_item['data']->get_data()['price'];
1706 if ($price > 0) {
1707 //grab item price from parent bundle if exist, return directly
1708 $cart_item_subtotal = $price * $cart_item['quantity'];
1709 $data = [
1710 'id' => strval($cart_item['product_id']),
1711 'quantity' => intval($cart_item['quantity']),
1712 'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2))
1713 ];
1714 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'data' => $data];
1715 }
1716 }
1717 //add bundle parent key to allowed array
1718 $allowed_bundle_parents[] = $cart_item['key'];
1719 }
1720
1721 if ($this->isProductVirtual($cart_item)) {
1722 //if the product is not found or virtual, skip it
1723 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1724 }
1725
1726 if (isset($cart_item['bundled_by'])) {
1727 if (!in_array($cart_item['bundled_by'], $allowed_bundle_parents)) {
1728 //skip bundle item children (price is on parent)
1729 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1730 }
1731 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
1732 //grab subtotal from bundle child
1733 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
1734 }
1735 }
1736
1737 return [
1738 'allowed_bundle_parents' => $allowed_bundle_parents,
1739 'data' => [
1740 'id' => strval($cart_item['product_id']),
1741 'quantity' => intval($cart_item['quantity']),
1742 'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2))
1743 ]
1744 ];
1745 }
1746
1747 /**
1748 * Get cart item subtotal
1749 *
1750 * @param $cart_item
1751 * @param $allowed_bundle_parents
1752 * @return array
1753 */
1754 public function getCartItemSubtotal($cart_item, $allowed_bundle_parents) {
1755
1756 $cart_item = $this->normalize_item_for_bundle_calc($cart_item);
1757
1758 $cart_item_subtotal = $cart_item['line_subtotal'];
1759
1760 //support for WooCommerce Product Bundles (only when product data is available; order items may have null)
1761 if (isset($cart_item['bundled_items'])) {
1762 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
1763 $price = $cart_item['data']->get_data()['price'];
1764 if ($price > 0) {
1765 //grab item price from parent bundle if exist, return directly
1766 $cart_item_subtotal = $price * $cart_item['quantity'];
1767 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal];
1768 }
1769 }
1770 //add bundle parent key to allowed array
1771 $allowed_bundle_parents[] = $cart_item['key'];
1772 }
1773
1774 if ($this->isProductVirtual($cart_item)) {
1775 //if the product is not found or virtual, skip it
1776 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1777 }
1778
1779 if (isset($cart_item['bundled_by'])) {
1780 if (!in_array($cart_item['bundled_by'], $allowed_bundle_parents)) {
1781 //skip bundle item children (price is on parent)
1782 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1783 }
1784 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
1785 //grab subtotal from bundle child
1786 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
1787 }
1788 }
1789 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal];
1790 }
1791
1792 /**
1793 * get cart shippable items to grab quote on RouteApp
1794 *
1795 * @param $cart
1796 * @param string $object
1797 * @return array
1798 */
1799 public function get_cart_shippable_items($cart, $object='cart')
1800 {
1801 $cartItems = [];
1802 if (!$cart) {
1803 return $cartItems;
1804 }
1805 $allowed_bundle_parents = [];
1806 if ($object=='cart') {
1807 foreach ( $cart->get_cart() as $cart_item ) {
1808 $cartItemData = $this->processCartItem($cart_item, $allowed_bundle_parents);
1809 $allowed_bundle_parents = $cartItemData['allowed_bundle_parents'];
1810 if (isset($cartItemData['data'])) {
1811 $cartItems[] = $cartItemData['data'];
1812 }
1813 }
1814 } else {
1815 foreach ( $cart->get_items() as $cart_item ) {
1816 $cartItemData = $this->processCartItem($cart_item, $allowed_bundle_parents);
1817 $allowed_bundle_parents = $cartItemData['allowed_bundle_parents'];
1818 if (isset($cartItemData['data'])) {
1819 $cartItems[] = $cartItemData['data'];
1820 }
1821 }
1822 }
1823
1824 return $cartItems;
1825 }
1826
1827 /**
1828 * get subtotal for shippable items only
1829 *
1830 * @param $cart
1831 * @param string $object
1832 * @return float|int|mixed
1833 */
1834 public function get_cart_subtotal_with_only_shippable_items($cart, $object='cart')
1835 {
1836 $subtotal = 0;
1837 if (!$cart) {
1838 return $subtotal;
1839 }
1840 $allowed_bundle_parents = [];
1841
1842 if ($object=='cart') {
1843 foreach ( $cart->get_cart() as $cart_item ) {
1844 $itemSubtotal = $this->getCartItemSubtotal($cart_item, $allowed_bundle_parents);
1845 $allowed_bundle_parents = $itemSubtotal['allowed_bundle_parents'];
1846 if (isset($itemSubtotal['subtotal'])) {
1847 $subtotal += $itemSubtotal['subtotal'];
1848 }
1849 }
1850 } else {
1851 foreach ( $cart->get_items() as $cart_item ) {
1852 $itemSubtotal = $this->getCartItemSubtotal($cart_item, $allowed_bundle_parents);
1853 $allowed_bundle_parents = $itemSubtotal['allowed_bundle_parents'];
1854 if (isset($itemSubtotal['subtotal'])) {
1855 $subtotal += $itemSubtotal['subtotal'];
1856 }
1857 }
1858 }
1859 return $subtotal;
1860 }
1861
1862 /**
1863 * Edit Avatax fee line (send tax class name instead item_id)
1864 */
1865 public function routeapp_edit_avatax_fee($fee_line, $fee = null)
1866 {
1867 if (isset($fee_line['description']) && $fee_line['description'] == $this->routeapp_get_insurance_label()) {
1868 $fee_line['itemCode'] = $this->routeapp_get_taxable_class();
1869 }
1870
1871 return $fee_line;
1872 }
1873
1874
1875 /**
1876 * This method adds to xml export an SKU tag in order to identify
1877 * Route Protection to ShipStation integration
1878 *
1879 * @param $order_xml
1880 * @return mixed
1881 */
1882 public function routeapp_add_sku_to_route_node($order_xml){
1883
1884 if ($order_xml instanceof DOMElement){
1885
1886 $items = $order_xml->getElementsByTagName('Items');
1887
1888 foreach ($items as $itemNode) {
1889 foreach ($itemNode->getElementsByTagName('Item') as $item) {
1890 foreach ($item->getElementsByTagName('Name') as $name) {
1891 if (trim($name->nodeValue) === self::ROUTE_LABEL)
1892 {
1893 $itemNode->removeChild($item);
1894 }
1895 }
1896 }
1897 }
1898
1899 }
1900
1901 return $order_xml;
1902 }
1903
1904 /**
1905 * Update order meta only if changed (HPOS & CPT safe)
1906 *
1907 * @return bool True if meta changed
1908 */
1909 public function updateRouteOrderMetaData(
1910 WC_Order $order,
1911 bool $is_hpos,
1912 string $meta_key,
1913 $meta_value
1914 ): bool {
1915 try {
1916 if ( ! $order || empty( $meta_key ) ) {
1917 return false;
1918 }
1919
1920 $normalized = is_scalar( $meta_value )
1921 ? (string) $meta_value
1922 : wp_json_encode( $meta_value );
1923
1924 if ( $is_hpos ) {
1925 $current = $order->get_meta( $meta_key, true );
1926 $current = is_scalar( $current ) ? (string) $current : wp_json_encode( $current );
1927
1928
1929 if ( (string) $current === (string) $normalized ) {
1930 return false;
1931 }
1932
1933 $order->update_meta_data( $meta_key, $normalized );
1934 return true;
1935
1936 }
1937
1938 $order_id = $order->get_id();
1939 $current = get_post_meta( $order_id, $meta_key, true );
1940 $current = is_scalar( $current ) ? (string) $current : wp_json_encode( $current );
1941
1942
1943 if ( (string) $current === (string) $normalized ) {
1944 return false;
1945 }
1946
1947 update_post_meta( $order_id, $meta_key, $normalized );
1948 return true;
1949
1950 } catch ( Exception $e ) {
1951 error_log(
1952 'RouteApp: error updating order meta (' . $meta_key . '): ' . $e->getMessage()
1953 );
1954 return false;
1955 }
1956 }
1957
1958 //MSS-6533
1959 /**
1960 * Resolve the local pickup flag in a FAIL-SAFE way.
1961 *
1962 * Priority:
1963 * 1. Order shipping items
1964 * 3. Checkout session (fallback only)
1965 *
1966 * @param WC_Order|null $order
1967 *
1968 * @return int 1 = local pickup, 0 = not local pickup
1969 */
1970 public function routeapp_resolve_ship_to_store_flag( WC_Order $order = null): int {
1971 $excluded_methods = $this->routeapp_get_excluded_shipping_methods();
1972 if ( empty( $excluded_methods ) || ! is_array( $excluded_methods ) ) {
1973 return 0;
1974 }
1975
1976 // Order-based (most reliable).
1977 if ( $order instanceof WC_Order ) {
1978 $shipping_items = $order->get_shipping_methods();
1979 if ( ! empty( $shipping_items ) ) {
1980 foreach ( $shipping_items as $shipping_item ) {
1981 if ( ! $shipping_item instanceof WC_Order_Item_Shipping ) {
1982 continue;
1983 }
1984 $method_id = $shipping_item->get_method_id();
1985 if ( empty( $method_id ) ) {
1986 continue;
1987 }
1988 $method_id = explode( ':', $method_id )[0];
1989 if ( in_array( $method_id, $excluded_methods, true ) ) {
1990 return 1;
1991 }
1992 }
1993 return 0;
1994 }
1995 }
1996
1997 // Session fallback (checkout only).
1998 if ( ! function_exists( 'WC' ) || ! WC()->session ) {
1999 return 0;
2000 }
2001 $chosen = WC()->session->get( 'chosen_shipping_methods' );
2002 $packages = WC()->session->get( 'shipping_for_package_0' );
2003 if ( empty( $chosen[0] ) || empty( $packages['rates'] ) ) {
2004 return 0;
2005 }
2006 $chosen_id = $chosen[0];
2007 if ( ! isset( $packages['rates'][ $chosen_id ] ) ) {
2008 return 0;
2009 }
2010 $method_id = explode( ':', $packages['rates'][ $chosen_id ]->id )[0];
2011 if ( in_array( $method_id, $excluded_methods, true ) ) {
2012 return 1;
2013 }
2014
2015 return 0;
2016 }
2017
2018 /**
2019 * Register callback for WooCommerce blocks checkout cart updates
2020 * This ensures the Route widget toggle properly updates the cart in blocks checkout
2021 *
2022 * @return void
2023 */
2024 public function routeapp_register_blocks_checkout_callback() {
2025 if ( function_exists( 'woocommerce_store_api_register_update_callback' ) ) {
2026 woocommerce_store_api_register_update_callback(
2027 array(
2028 'namespace' => 'route-widget-integration',
2029 'callback' => function( $data ) {
2030 // Update session if checkbox value is provided
2031 if ( isset( $data['checkbox'] ) && WC()->session ) {
2032 $checkboxValue = filter_var( $data['checkbox'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
2033
2034 // Treat invalid values as false to keep session state deterministic.
2035 WC()->session->set( 'checkbox_checked', true === $checkboxValue );
2036 }
2037
2038 // The cart will be recalculated automatically, triggering woocommerce_cart_calculate_fees
2039 return true;
2040 },
2041 )
2042 );
2043 }
2044 }
2045
2046 /**
2047 * Inject Route widget before the checkout block order summary (default placement for blocks checkout).
2048 *
2049 * @param string $block_content The block content.
2050 * @param array $block The full block, including name and attributes.
2051 * @param object $instance The block instance.
2052 * @return string Modified block content with Route widget prepended when applicable.
2053 */
2054 public function routeapp_checkout_block_before_order_summary( $blockContent, $block, $instance ) {
2055 if ( is_admin() && ! wp_doing_ajax() ) {
2056 return $blockContent;
2057 }
2058
2059 if ( $this->route_block_rendered || $this->is_route_block_in_checkout_page() ) {
2060 return $blockContent;
2061 }
2062
2063 if ( ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->session ) {
2064 return $blockContent;
2065 }
2066 if ( empty( $this->routeapp_get_public_token() ) ) {
2067 return $blockContent;
2068 }
2069
2070 WC()->cart->calculate_totals();
2071
2072 $routeHtml = do_shortcode( '[route]' );
2073 if ( empty( $routeHtml ) ) {
2074 return $blockContent;
2075 }
2076
2077 return $routeHtml . $blockContent;
2078 }
2079
2080 /**
2081 * Register the Route Protection custom checkout block.
2082 * Hooked to 'init'.
2083 */
2084 public function routeapp_register_checkout_block() {
2085 if ( ! function_exists( 'register_block_type' ) ) {
2086 return;
2087 }
2088
2089 $block_dir = plugin_dir_path( dirname( __FILE__ ) ) . 'blocks/route-protection';
2090 if ( ! file_exists( $block_dir . '/block.json' ) ) {
2091 return;
2092 }
2093
2094 wp_register_script(
2095 'route-protection-editor',
2096 plugins_url( 'blocks/route-protection/index.js', dirname( __FILE__ ) ),
2097 array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wc-blocks-checkout' ),
2098 defined( 'ROUTEAPP_VERSION' ) ? ROUTEAPP_VERSION : '1.0.0',
2099 true
2100 );
2101
2102 register_block_type(
2103 $block_dir,
2104 array(
2105 'editor_script' => 'route-protection-editor',
2106 'render_callback' => array( $this, 'render_route_protection_block' ),
2107 )
2108 );
2109 }
2110
2111 /**
2112 * Server-side render callback for the route/route-protection block.
2113 *
2114 * @param array $attributes Block attributes.
2115 * @param string $content Block inner content.
2116 * @return string
2117 */
2118 public function render_route_protection_block( $attributes, $content ) {
2119 if ( is_admin() && ! wp_doing_ajax() ) {
2120 return '';
2121 }
2122 if ( ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->session ) {
2123 return '';
2124 }
2125 if ( empty( $this->routeapp_get_public_token() ) ) {
2126 return '';
2127 }
2128
2129 WC()->cart->calculate_totals();
2130
2131 $routeHtml = do_shortcode( '[route]' );
2132
2133 $this->route_block_rendered = true;
2134
2135 return $routeHtml;
2136 }
2137
2138 /**
2139 * Register the Route Protection IntegrationInterface with WooCommerce Blocks.
2140 *
2141 * @param \Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry $integration_registry
2142 */
2143 public function routeapp_register_blocks_integration( $integration_registry ) {
2144 $file = plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-routeapp-blocks-integration.php';
2145 if ( ! class_exists( 'Routeapp_Blocks_Integration' ) && file_exists( $file ) ) {
2146 require_once $file;
2147 }
2148 if ( class_exists( 'Routeapp_Blocks_Integration' ) ) {
2149 $integration_registry->register( new Routeapp_Blocks_Integration() );
2150 }
2151 }
2152
2153 /**
2154 * Check whether the checkout page contains the route/route-protection block.
2155 *
2156 * @return bool
2157 */
2158 private function is_route_block_in_checkout_page() {
2159 if ( ! function_exists( 'wc_get_page_id' ) || ! function_exists( 'has_block' ) ) {
2160 return false;
2161 }
2162
2163 $checkout_page_id = wc_get_page_id( 'checkout' );
2164 if ( $checkout_page_id <= 0 ) {
2165 return false;
2166 }
2167
2168 $post = get_post( $checkout_page_id );
2169 if ( ! $post ) {
2170 return false;
2171 }
2172
2173 return has_block( 'route/route-protection', $post );
2174 }
2175
2176 }
2177
2178