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

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