PluginProbe
Route ‑ Shipping Protection / 2.4.5
Route ‑ Shipping Protection v2.4.5
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.5, at public/class-routeapp-public.php

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