# routeapp/2.4.18/public/class-routeapp-public.php

Route ‑ Shipping Protection, version 2.4.18. 2,601 lines.

- Page: https://pluginprobe.com/plugins/routeapp/2.4.18/code/public/class-routeapp-public.php
- Raw: https://pluginprobe.com/plugins/routeapp/2.4.18/raw/public/class-routeapp-public.php
- Modified: 2026-09-08T22:04:28+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/routeapp/2.4.18/code/public/class-routeapp-public.php#L10-L20`.

```php
<?php
/**
 * The public-facing functionality of the plugin.
 *
 * @link       https://route.com/
 * @since      1.0.0
 *
 * @package    Routeapp
 * @subpackage Routeapp/public
 */

use Automattic\WooCommerce\Utilities\OrderUtil;

/**
 * The public-facing functionality of routeapp.
 *
 *
 * @package    Routeapp
 * @subpackage Routeapp/public
 * @author     Route App <support@route.com>
 */

class Routeapp_Public {

    const HTTP_SUCCESS_CODE = 200;
    const HTTP_BAD_REQUEST_CODE = 400;
    const HTTP_CREATED_CODE = 201;
    const HTTP_CONFLICT_CODE = 409;
    const HTTP_NOT_FOUND = 404;
    const HTTP_INTERNAL_SERVER_ERROR_CODE = 500;
    const DEFAULT_MAX_USD_SUBTOTAL_ALLOWED = 5000;
    const DEFAULT_MIN_USD_SUBTOTAL_ALLOWED = 0;
    const ROUTE_LABEL = 'Route Shipping Protection';
    const DEFAULT_CHECKOUT_WEBHOOK =  'woocommerce_checkout_before_order_review';
    const ROUTE_PROTECTION_SKU = 'ROUTEINS';
    const ROUTE_FEE_ITEM_META = '_routeapp_protection_fee';
    const ROUTE_FEE_SKU_META = '_routeapp_protection_sku';
    const ALLOWED_QUOTE_TYPE = 'paid_by_customer';
    const PICKUP_SHIPPING_METHOD_IDS = array( 'local_pickup', 'pickup_location' );

	const AMPLITUDE_API_KEY = [
		'production' => '6d4f9158886e0c6d78e3bd97663ba920',
		'stage' => '99a869d62986f6415610c6741057bc92',
		'dev' => '527af176070cf4b775c6a718f0737f0a'
	];

    /**
     * The ID of this plugin.
     *
     * @since    1.0.0
     * @access   private
     * @var      string    $plugin_name    The ID of this plugin.
     */
    private $plugin_name;

	private $route_insurance_data = null;

    /**
     * The version of this plugin.
     *
     * @since    1.0.0
     * @access   private
     * @var      string    $version    The current version of this plugin.
     */
    private $version;

    /**
     * Routeapp API instance
     * @since    1.0.0
     * @access   private
     * @var 	string 		$routeapp_api 	Routeapp API Client
     */
    public $routeapp_api_client;


    /**
     * Route Shipment Tracking Module
     */
    private $routeapp_shipment_tracking;

    /**
     * Last Request
     */
    private $routeapp_last_request;

    /**
     * Route Logger
     */
    private $routeapp_logger;

    /**
     * Route Admin Fee
     */
    private $routeapp_admin_fee;

    /**
     * Route Setup
     */
    private $route_app_setup_helper;

    /**
     * Whether the custom Route checkout block has been rendered on this request.
     */
    private $route_block_rendered = false;

    /**
     * Initialize the class and set its properties.
     *
     * @since    1.0.0
     * @param      string    $plugin_name       The name of the plugin.
     * @param      string    $version    The version of this plugin.
     */
    public function __construct($plugin_name, $version)
    {

        $this->plugin_name = $plugin_name;
        $this->version = $version;
        $this->routeapp_api_client = new Routeapp_API_Client($this->routeapp_get_public_token(), $this->routeapp_get_secret_token());
        $this->routeapp_shipment_tracking = new Routeapp_Shipment_Tracking();
        $this->routeapp_logger = new Routeapp_Logger_Sentry();
        $this->routeapp_admin_fee = new Routeapp_Admin_Route_Fee();
        $this->route_app_setup_helper = new Route_Setup();
        Route_Setup::ensure_store_hash();
        $this->add_actions_and_filters();
    }

    public function add_actions_and_filters() {
        //CHECKOUT HOOK
        $checkout_hook = $this->routeapp_get_checkout_hook();
        add_action($checkout_hook, array($this, 'checkout_route_insurance'), 1, 0);

        add_action('woocommerce_cart_calculate_fees', array($this, 'checkout_route_insurance_fee'), 20, 1);
        add_action('wp_ajax_woo_get_ajax_data', array($this, 'checkout_route_insurance_set_session'));
        add_action('wp_ajax_nopriv_woo_get_ajax_data', array($this, 'checkout_route_insurance_set_session'));

        add_action('wp_ajax_woo_check_widget', array($this, 'check_route_widget'));
        add_action('wp_ajax_nopriv_woo_check_widget', array($this, 'check_route_widget'));

        add_action( 'woocommerce_checkout_create_order_line_item', [ $this, 'routeapp_checkout_create_order_line_item' ], 10, 4 );
        add_action( 'woocommerce_checkout_create_order_fee_item', [ $this, 'routeapp_checkout_create_order_fee_item' ], 10, 4 );
        add_action( 'woocommerce_ajax_add_order_item_meta', [$this, 'routeapp_ajax_add_order_item_meta'], 10, 2 );
        add_filter( 'woocommerce_hidden_order_itemmeta', [$this,'routeapp_woocommerce_hidden_order_itemmeta'], 10, 1);

        add_action( 'added_post_meta', array( $this, 'routeapp_update_tracking_order_api' ), 20, 3 );
        add_action( 'updated_post_meta', array( $this, 'routeapp_update_tracking_order_api' ), 20, 3 );

        add_action('woocommerce_after_order_object_save', array($this, 'routeapp_update_tracking_order_api_HPOS'), 20, 2);
        
        add_action( 'added_order_item_meta', array( $this, 'routeapp_update_tracking_order_item_api' ), 20, 3 );
        add_action( 'updated_order_item_meta', array( $this, 'routeapp_update_tracking_order_item_api' ), 20, 3 );
        add_action('woocommerce_order_status_processing', array($this, 'routeapp_mark_order'), 10, 1);
        add_action('woocommerce_order_status_completed', array($this, 'routeapp_mark_order'), 8, 2);
        add_action('woocommerce_new_order', array($this, 'routeapp_save_quote_to_order'), 10, 1);
        add_action('rest_api_init', array($this, 'routeapp_register_status_route'));
        add_action('rest_api_init', array($this, 'routeapp_register_setup_route'));
        add_action('wp_ajax_get_route_checkout', array($this, 'routeapp_register_checkout_route'));
        add_action('wp_ajax_nopriv_get_route_checkout', array($this, 'routeapp_register_checkout_route'));
        add_action('wp_head', array($this, 'wc_routeapp_ajaxurl'));
        add_action('woocommerce_init', array($this, 'register_shortcode'));
        add_action('woocommerce_rest_prepare_order_note', array($this, 'check_order_note'));
        add_action('wp_insert_comment', array($this, 'check_order_note_by_note_id'));

        add_action('wp_ajax_routeapp_add_admin_fee', array($this, 'routeapp_add_admin_fee'));
        add_action('wp_ajax_nopriv_routeapp_add_admin_fee', array($this, 'routeapp_add_admin_fee'));
        add_action('wp_ajax_routeapp_remove_admin_fee', array($this, 'routeapp_remove_admin_fee'));
        add_action('wp_ajax_nopriv_routeapp_remove_admin_fee', array($this, 'routeapp_remove_admin_fee'));
        add_action('wp_ajax_routeapp_refresh_admin_fee_button', array($this, 'routeapp_refresh_admin_fee_button'));
        add_action('wc_avatax_api_fee_line_data', array($this, 'routeapp_edit_avatax_fee'));

        // ShipStation: exclude Route fee on XML (legacy) and REST (4.9+) export paths.
        add_filter('woocommerce_shipstation_no_shipping_item', array($this, 'routeapp_exclude_route_from_shipstation'), 10, 3);
        add_filter('woocommerce_shipstation_fulfillment_item', array($this, 'routeapp_strip_route_from_shipstation_fulfillment_item'), 10, 4);
        add_filter('woocommerce_shipstation_export_order_xml', array($this, 'routeapp_strip_route_from_shipstation_xml'), 10, 1);

        add_action('woocommerce_order_details_after_order_table', array($this, 'order_updates_route_tracking'), 1, 1);

        add_action('woocommerce_blocks_checkout_update_order_meta', array($this, 'routeapp_save_order_status_to_order'), 10, 1);
        add_action('woocommerce_store_api_checkout_update_order_meta', array($this, 'routeapp_save_order_status_to_order'), 10, 1);

        // Register callback for WooCommerce blocks checkout cart updates
        add_action('woocommerce_blocks_loaded', array($this, 'routeapp_register_blocks_checkout_callback'));

        // Inject Route widget before order summary in checkout block (default placement)
        add_filter('render_block_woocommerce/checkout-order-summary-block', array($this, 'routeapp_checkout_block_before_order_summary'), 10, 3);

        // Register the Route Protection custom checkout block
        add_action('init', array($this, 'routeapp_register_checkout_block'));

        // Register IntegrationInterface for WooCommerce Blocks
        add_action('woocommerce_blocks_checkout_block_registration', array($this, 'routeapp_register_blocks_integration'));

        // Enqueue widget assets early on cart/checkout for stable script order with Blocks SSR.
        add_action('wp_enqueue_scripts', array($this, 'routeapp_maybe_enqueue_widget_assets'), 20);
        add_action('woocommerce_blocks_enqueue_checkout_block_scripts_before', array($this, 'routeapp_maybe_enqueue_widget_assets'));
    }

    /**
     * Hide order line_item meta_data from Route App
     *
     * @param $arr
     * @return mixed
     */
    function routeapp_woocommerce_hidden_order_itemmeta($arr) {
        $arr[] = '_routeapp_source_product_id';
        $arr[] = '_routeapp_image_url';
        $arr[] = '_routeapp_virtual';
        $arr[] = '_routeapp_weight_value';
        $arr[] = '_routeapp_weight_unit';
        $arr[] = '_routeapp_origin_location';
        $arr[] = self::ROUTE_FEE_ITEM_META;
        $arr[] = self::ROUTE_FEE_SKU_META;
        return $arr;
    }

    /**
     * Fill metadata
     *
     * @param $item WC_Order_Item
     * @param $product WC_Product
     * @param $entity_type enum('order_item','item')
     * @return void
     */
    public static function fillMetaData($item, $product, $entity_type='order_item') {

        $sourceProductId = $product->get_id();
        $productImageUrl = (wp_get_attachment_url($product->get_image_id())) ? wp_get_attachment_url($product->get_image_id()) : null;
        $weightValue = $product->get_weight();

        $originLocation = json_encode([
            'address' => get_option('woocommerce_store_address'),
            'address2' => get_option('woocommerce_store_address_2'),
            'store_city' => get_option('woocommerce_store_city'),
            'store_postcode' => get_option('woocommerce_store_postcode'),
            'default_country' => get_option('woocommerce_default_country'),
        ]);


        switch($entity_type) {
            case 'order_item':
                //add source_product_id as order line_item meta_data
                self::addItemMeta($item, '_routeapp_source_product_id', $sourceProductId);

                //add image_url as order line_item meta_data
                self::addItemMeta($item, '_routeapp_image_url', $productImageUrl);

                //add weight_value as order line_item meta_data
                self::addItemMeta($item, '_routeapp_weight_value', $weightValue);

                //add weight_unit as order line_item meta_data
                self::addItemMeta($item, '_routeapp_weight_unit', get_option('woocommerce_weight_unit'));

                //check if product is virtual
                if ($product->is_virtual()) {
                    //add virtual as order line_item meta_data
                    self::addItemMeta($item, '_routeapp_virtual', 1);
                }
                //add origin location as order line_item meta_data
                self::addItemMeta($item, '_routeapp_origin_location', $originLocation);
                break;
            case 'item':
                //add source_product_id as order line_item meta_data
                self::addOrderItemMeta($item->get_id(), '_routeapp_source_product_id', $sourceProductId);

                //add image_url as order line_item meta_data
                self::addOrderItemMeta($item->get_id(), '_routeapp_image_url', $productImageUrl);

                //add weight_value as order line_item meta_data
                self::addOrderItemMeta($item->get_id(), '_routeapp_weight_value', $weightValue);

                //add weight_unit as order line_item meta_data
                self::addOrderItemMeta($item->get_id(), '_routeapp_weight_unit', get_option('woocommerce_weight_unit'));

                //check if product is virtual
                if ($product->is_virtual()) {
                    //add virtual as order line_item meta_data
                    self::addOrderItemMeta($item->get_id(), '_routeapp_virtual', 1);
                }
                //add origin location as order line_item meta_data
                self::addOrderItemMeta($item->get_id(), '_routeapp_origin_location', $originLocation);
                break;
        }
    }

    /**
     * Add order line_item meta_data on ajax add to cart
     *
     * @param $item_id String
     * @param $item WC_Order_Item
     * @return false|void
     */
    public static function routeapp_ajax_add_order_item_meta( $item_id, $item) {
        $product = $item->get_product(); // Get the WC_Product object
        if ($product instanceof WC_Product) {
            try {
                self::fillMetaData($item, $product, 'item');
            }catch (Exception $exception) {
                return false;
            }
        }
    }

    /**
     * @param $itemId WC_Order_Item ID
     * @param $key String
     * @param $value String
     * @return bool
     */
    private static function addOrderItemMeta($itemId, $key, $value) {
        try {
            $fieldExists = wc_get_order_item_meta( $itemId, $key, true);
            if (!$fieldExists) {
                wc_add_order_item_meta($itemId, $key, $value);
            }
        }catch (Exception $exception) {
            return false;
        }
        return true;
    }

    /**
     * @param $item WC_Order_Item
     * @param $key String
     * @param $value String
     * @return bool
     */
    private static function addItemMeta($item, $key, $value) {
        try {
            $fieldExists =  $item->get_meta($key, true);
            if (!$fieldExists) {
                $item->add_meta_data( $key, $value );
            }
        }catch (Exception $exception) {
            return false;
        }
        return true;
    }

    /**
     * Add order line_item meta_data on checkout add to cart
     *
     * @param $item WC_Order_Item
     * @param $cart_item_key String
     * @param $values Object
     * @param $order WC_Order
     * @return false|void
     */
    public function routeapp_checkout_create_order_line_item( $item, $cart_item_key, $values, $order ) {
        if ( $order instanceof WC_Order ) {
            $product = $item->get_product(); // Get the WC_Product object
            if ($product instanceof WC_Product) {
                try {
                    self::fillMetaData($item, $product, 'order_item');
                }catch (Exception $exception) {
                    return false;
                }
            }
        }
    }

    /**
     * Tag the Route protection fee on checkout so exporters can identify it
     * without relying on a translated fee name.
     *
     * Accepted limitation: WC cart fees have no SKU/meta, so we match by name
     * here. That is reliable at creation time because Route just set the fee
     * name via routeapp_get_insurance_label() in the same request/locale.
     * Once marked, later lookups use ROUTE_FEE_ITEM_META / ROUTE_FEE_SKU_META.
     *
     * @param WC_Order_Item_Fee $item    Fee order item.
     * @param string            $fee_key Cart fee key.
     * @param object            $fee     Cart fee object.
     * @param WC_Order          $order   Order being created.
     */
    public function routeapp_checkout_create_order_fee_item( $item, $fee_key, $fee, $order ) {
        $fee_name = '';
        if ( is_object( $fee ) && isset( $fee->name ) ) {
            $fee_name = $fee->name;
        } elseif ( is_callable( array( $item, 'get_name' ) ) ) {
            $fee_name = $item->get_name();
        }

        if ( self::routeapp_is_route_protection_fee_name( $fee_name ) ) {
            self::routeapp_mark_protection_fee_item( $item );
        }
    }

    public function routeapp_add_admin_fee() {
        $this->routeapp_admin_fee->routeapp_add_admin_fee();
    }

    public function routeapp_remove_admin_fee() {
        $this->routeapp_admin_fee->routeapp_remove_admin_fee();
    }

    public function routeapp_refresh_admin_fee_button() {
        $this->routeapp_admin_fee->routeapp_refresh_admin_fee_button();
    }

    public function register_shortcode() {
        add_shortcode( 'route', array( $this, 'route_widget_shortcode' ) );
    }

    public function get_allowed_quote_type() {
        return self::ALLOWED_QUOTE_TYPE;
    }

    public function check_route_widget() {
        if (!isset($_POST) || !$_POST['shipping_method']) {
            wp_send_json_error('its missing shipping method on the request');
            return;
        }
        $shipping_method = isset($_POST['shipping_method']) ? $_POST['shipping_method'] : '';
        $response = [];
        $response['show_widget'] =  $this->routeapp_is_shipping_method_allowed($shipping_method);
        wp_send_json_success( $response );
        return;
    }

    /**
     * set routeapp session
     *
     * @since    1.0.0
     */
    public function checkout_route_insurance_set_session() {
        if ( isset($_POST['checkbox']) && $_POST['checkbox'] === 'true' ){
            WC()->session->set('checkbox_checked', true );
        } else {
            WC()->session->set( 'checkbox_checked', false );
        }
    }

    /**
     * Add route insurance fee in checkout and cart.
     * Only runs during woocommerce_cart_calculate_fees. The old
     * woocommerce_checkout_before_order_review binding is gone — it fired
     * after totals were already calculated and re-added the fee after opt-out.
     *
     * @since    1.0.0
     * @param  Object $cart_or_checkout
     */
    public function checkout_route_insurance_fee( $cart_or_checkout ) {
        if ( (is_admin() && ! defined( 'DOING_AJAX' )) ) return;

        // Widget AJAX / fragment hooks must not add fees (re-adds after opt-out, races coupons).
        if ( function_exists( 'doing_action' ) && ! doing_action( 'woocommerce_cart_calculate_fees' ) ) {
            return;
        }

        // Determine if we received a WC_Cart or WC_Checkout object
        $is_checkout_object = is_a( $cart_or_checkout, 'WC_Checkout' );
        $cart = $is_checkout_object ? WC()->cart : $cart_or_checkout;
        
        // Use WC()->cart as fallback if not a valid WC_Cart
        if ( is_null( $cart ) || ! is_a( $cart, 'WC_Cart' ) ) {
            $cart = WC()->cart;
        }
        
        $checkbox = WC()->session->get( 'checkbox_checked' );

        $cartRef = $cart->get_cart_hash();
        $cartTotal = round($this->get_cart_subtotal_with_only_shippable_items($cart), 2);
        $currency = get_woocommerce_currency();
        $cartItems = $this->get_cart_shippable_items($cart);
        $route_insurance_quote = $this->routeapp_get_quote_from_api($cartRef, $cartTotal, $currency, $cartItems);
		$this->route_insurance_data = $route_insurance_quote;

        $route_insurance_amount = false;
        //only add fee if the quote is covered by customer and has a valid amount
        if (isset($route_insurance_quote->premium->amount) &&
            (isset($route_insurance_quote->payment_responsible->type)) && $route_insurance_quote->payment_responsible->type==self::ALLOWED_QUOTE_TYPE) {
            $route_insurance_amount = $route_insurance_quote->premium->amount;
        }
       
        if (is_null($checkbox) && 
            isset($route_insurance_quote->payment_responsible) && 
            property_exists($route_insurance_quote->payment_responsible, 'ToggleState')) {
            $checkbox = $route_insurance_quote->payment_responsible->ToggleState;
            WC()->session->set('checkbox_checked', $checkbox);
        }

        // First checkout paint still defaults ON when ToggleState is missing
        // (pre-existing behavior). Do not default ON during checkout AJAX —
        // that re-added the fee after opt-out when the session was empty.
        if ( $this->routeapp_should_default_fee_opt_in( $checkbox ) ) {
            $checkbox = true;
            if ( WC()->session ) {
                WC()->session->set( 'checkbox_checked', true );
            }
        }

        Route_Setup::set_setup_check_widget(Route_Setup::SETUP_CHECK_WIDGET_PHP);
        if ( ($checkbox === TRUE || $checkbox === 1) && $route_insurance_amount > 0 && $this->routeapp_is_shipping_method_allowed()) {
            $cart->add_fee( $this->routeapp_get_insurance_label(),
                $route_insurance_amount,
                $this->routeapp_get_fee_taxable(),
                $this->routeapp_get_taxable_class());
        }
    }

    /**
     * WooCommerce / CheckoutWC totals refresh (update_order_review, etc.).
     *
     * @return bool
     */
    public function routeapp_is_checkout_ajax_refresh() {
        if ( ! empty( $_REQUEST['wc-ajax'] ) ) {
            return true;
        }

        return defined( 'DOING_AJAX' ) && DOING_AJAX;
    }

    /**
     * Keep the pre-existing first-load default (fee ON) when there is no
     * session choice and no ToggleState. Skip that default on AJAX refreshes.
     *
     * @param mixed $checkbox Session checkbox value.
     * @return bool
     */
    public function routeapp_should_default_fee_opt_in( $checkbox ) {
        return is_null( $checkbox )
            && function_exists( 'is_checkout' )
            && is_checkout()
            && ! $this->routeapp_is_checkout_ajax_refresh();
    }

    /**
     * Create checkout
     * @since    1.0.0
     *
     */
    public function checkout_route_insurance() {
        echo do_shortcode('[route]');
    }

    public function order_updates_route_tracking($order) {
        if ($this->routeapp_route_show_order_updates()) {
            echo $this->route_show_thankyoupage_asset($order);
        }
    }

    public function route_show_thankyoupage_asset($order) {
        $output = '';

        $site = parse_url(get_site_url(get_current_blog_id()), PHP_URL_HOST);
        $response = Routeapp_API_Client::getInstance()->asset_settings($site);

        if (is_wp_error($response) ||
            (isset($response['response']['code']) && $response['response']['code'] == 404) ||
            is_null($response['body'])) {
            return $output;
        } else {

            $body = json_decode($response['body']);

            if (!is_null($body) &&
                !is_null($body->asset_settings) &&
                !is_null($body->asset_settings->asset_live) &&
                $body->asset_settings->asset_live) {
                $raw_html = $body->asset_settings->asset_content->raw_html;
                $css = $body->asset_settings->asset_content->css_url;
                $js = isset($body->asset_settings->asset_content->js_url) ? $body->asset_settings->asset_content->js_url : '';
                $jsPath = '';
                if (!empty($js)) {
                    $jsPath = '<script type="text/javascript" src="' . $js . '"></script>';
                }
                $output = '<link rel="stylesheet" href="' . $css . '" type="text/css" />' .
                    $jsPath .
                    '<section class=woocommerce-order-updates>' .
                    '<h2 class="woocommerce-order-updates__title">Order Updates</h2>' .
                    '</section>' .
                    '<div><span class="os-order-number" style="display:none;">'.$order->get_id().'</span></div>'.
                    '<div>' .
                    $raw_html .
                    '</div>' .
                    '<div>&nbsp;</div>';

                wp_enqueue_script($this->plugin_name . '-analytics', 'https://cdn.routeapp.io/route-analytics/route-analytics.js', array(), $this->version, false);
                wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'js/routeapp-analytics.js', array('jquery'), $this->version, false);
            }
        }

        return $output;
    }

    /**
     * Enqueue Route widget JS/CSS with correct dependency order.
     * routeapp-public-pbc.js must load after route-protection-widget.js (Route.Protection).
     */
    public function routeapp_enqueue_widget_assets() {
        if ( is_admin() || empty( $this->routeapp_get_public_token() ) ) {
            return;
        }
        if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
            return;
        }

        $custom_env = getenv( 'ROUTEAPP_ENVIRONMENT_ENDPOINT' );
        if ( is_null( $custom_env ) || ! $custom_env ) {
            $custom_env = isset( $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] ) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : '';
        }
        $custom_env = empty( $custom_env ) ? 'production' : $custom_env;
        if ( ! isset( self::AMPLITUDE_API_KEY[ $custom_env ] ) ) {
            $custom_env = 'production';
        }

        $widget_handle = $this->plugin_name . '-widget';
        wp_enqueue_script(
            $widget_handle,
            'https://protection-widget.route.com/route-protection-widget.js',
            array(),
            $this->version,
            true
        );

        $amplitude_key = self::AMPLITUDE_API_KEY[ $custom_env ];

        // Load Amplitude SDK (standard version, not snippet)
        wp_enqueue_script( 'amplitude-js', "https://cdn.amplitude.com/script/$amplitude_key.js", array(), null, true );
        // Enqueue Amplitude Analytics module (depends on jQuery and Amplitude SDK)
        wp_enqueue_script(
            $this->plugin_name . '-amplitude-analytics',
            plugin_dir_url( __FILE__ ) . 'js/amplitude-analytics.js',
            array( 'jquery', 'amplitude-js' ),
            $this->version,
            true
        );
        $pbc_deps = array( 'jquery', $widget_handle, $this->plugin_name . '-amplitude-analytics' );
        if ( $this->routeapp_should_add_wc_blocks_checkout_dependency() ) {
            $pbc_deps[] = 'wc-blocks-checkout';
        }

        /**
         * Filter script dependencies for routeapp-public-pbc.js.
         *
         * @param string[] $pbc_deps Script handles routeapp-public-pbc.js depends on.
         */
        $pbc_deps = apply_filters( 'routeapp_pbc_script_dependencies', $pbc_deps );

        // Main script depends on amplitude-analytics when available.
        wp_enqueue_script(
            $this->plugin_name,
            plugin_dir_url( __FILE__ ) . 'js/routeapp-public-pbc.js',
            $pbc_deps,
            $this->version,
            true
        );

        /**
         * Calculate total Route fee
         * Uses the premium amount from route insurance data if available,
         * otherwise defaults to 0.98 as a fallback fee amount
         */
        $totalRouteFee = ( is_object( $this->route_insurance_data ) && isset( $this->route_insurance_data->premium->amount ) )
            ? (float) $this->route_insurance_data->premium->amount
            : 0.98;

        // Pass cart data to JavaScript - send to amplitude-analytics.js
        wp_localize_script(
            $this->plugin_name . '-amplitude-analytics',
            'cartData',
            array(
                'cartId'        => WC()->cart->get_cart_hash(),
                'amplitudeKey'  => $amplitude_key,
                'widgetId'      => uniqid( 'widget-', true ),
                'subtotal'      => round( $this->get_cart_subtotal_with_only_shippable_items( WC()->cart ), 2 ),
                'totalRouteFee' => $totalRouteFee,
                'environment'   => $custom_env,
                'currency'      => get_woocommerce_currency(),
                'merchantId'    => $this->routeapp_api_client->get_merchant_id(),
            )
        );

        wp_localize_script(
            $this->plugin_name,
            'routeappWidgetConfig',
            array(
                'excludedShippingMethods' => array_values( (array) $this->routeapp_get_excluded_shipping_methods() ),
            )
        );

        wp_enqueue_style(
            $this->plugin_name . '-widget',
            plugin_dir_url( __FILE__ ) . 'css/routeapp-widget.css',
            array(),
            $this->version,
            'all'
        );
    }

    /**
     * Enqueue widget assets on cart, checkout, and Blocks checkout script hook.
     */
    public function routeapp_maybe_enqueue_widget_assets() {
        if ( is_admin() || empty( $this->routeapp_get_public_token() ) ) {
            return;
        }
        if ( function_exists( 'is_cart' ) && function_exists( 'is_checkout' ) && ! is_cart() && ! is_checkout() ) {
            return;
        }
        $this->routeapp_enqueue_widget_assets();
    }

    public function route_widget_shortcode()
    {
        if(is_admin() || empty($this->routeapp_get_public_token())) return;

        // Enqueue only outside cart/checkout; wp_enqueue_scripts handles those pages.
        if ( function_exists( 'is_cart' ) && function_exists( 'is_checkout' ) && ! is_cart() && ! is_checkout() ) {
            $this->routeapp_enqueue_widget_assets();
        }

        $custom_env = getenv('ROUTEAPP_ENVIRONMENT_ENDPOINT');
        if (is_null($custom_env) || !$custom_env) {
            $custom_env = isset($_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT']) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : '';
        }
        $custom_env = empty($custom_env) ? 'production' : $custom_env;

        global $wp;
        $parsedUrl = parse_url(home_url( $wp->request));

        switch($custom_env) {
            case 'stage':
                $custom_env = 'Route.Environment.Dev';
                break;
            default:
                $custom_env = 'Route.Environment.Production';
                break;
        }
        $checkbox = WC()->session->get( 'checkbox_checked' );
        $checkbox = $checkbox ? 'Route.Coverage.ActiveByDefault' : 'Route.Coverage.InactiveByDefault';

        $divId = is_checkout() ? 'route-checkout-radio' : 'route-cart-radio';

        $return = '<div id="'.$divId.'">' .
            '<input type="hidden" value="'. get_woocommerce_currency() .'" class="routeapp-currency">'.
            '<input type="hidden" value="'. round($this->get_cart_subtotal_with_only_shippable_items(WC()->cart), 2) .'" class="routeapp-subtotal">'.
            '<input type="hidden" value="' . admin_url('admin-ajax.php') . '" class="routeapp-ajax-url">'.
            '<input type="hidden" value="' . $parsedUrl['host'] . '" class="routeapp-store-domain">'.
            '<input type="hidden" value="' . get_option('blogname') . '" class="routeapp-store-name">'.
            '<input type="hidden" value="' . $this->routeapp_api_client->get_merchant_id() . '" class="routeapp-merchant-id">'.
            '<input type="hidden" value="' . $custom_env . '" class="routeapp-env">'.
            '<input type="hidden" value="' . $checkbox .'" class="routeapp-checkbox-cookie">';

        $chosen_methods = ( WC()->session ) ? WC()->session->get( 'chosen_shipping_methods' ) : array();
        if ( ! empty( $chosen_methods[0] ) && ! $this->routeapp_is_shipping_method_allowed( $chosen_methods[0] ) ) {
            $return .= '<input type="hidden" value="true" class="routeapp-invalid-shipping-method">';
        }
        return $return . '<div id="RouteWidget" class="route-div"></div></div>' ;
    }


    public function wc_routeapp_ajaxurl() {
        echo '<script type="text/javascript">' .
            'var wc_routeapp_ajaxurl = "' . admin_url('admin-ajax.php') . '"' .
            '</script>';
    }

    /**
     * get amount from RouteApp API
     *
     * @param $cartRef
     * @param $cartTotal
     * @param $currency
     * @param $cartItems
     * @return false|mixed
     */
    public function routeapp_get_quote_from_api($cartRef, $cartTotal, $currency, $cartItems) {
        if (!$cartRef) return false;
        if (!$cartTotal) return false;
        $currency = !$currency ? get_woocommerce_currency() : $currency;

        $response = $this->routeapp_api_client->get_quote($cartRef, $cartTotal, $currency, $cartItems);

        if (is_wp_error($response)) {
            return false;
        } else {
            if (isset($response['response']['code']) && $response['response']['code'] == 401) {
                return false;
            }
            $price_data = json_decode($response['body']);
            Route_Setup::set_setup_check_widget(Route_Setup::SETUP_CHECK_WIDGET_JS);
            return $price_data;
        }
    }

    /**
     * create order data sending for API
     * @since    1.0.0
     * @param  integer $order_id
     */
    public function create_order_data_api($order_id) {

        $order = wc_get_order( $order_id );

        if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
            && OrderUtil::custom_orders_table_usage_is_enabled() ) {
            // HPOS usage is enabled.
            $quote = $order->get_meta('_routeapp_quote');
        } else {
            // Traditional CPT-based orders are in use.
            $quote = get_post_meta($order->get_id(), '_routeapp_quote', true);
        }

        $insurance_selected = false;
        $insurance_amount = false;

        foreach ($order->get_items('fee') as $fee) {
            if (self::routeapp_is_route_protection_fee($fee)) {
                $insurance_selected = true;
                $insurance_amount = $fee->get_amount();
                if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
                    && OrderUtil::custom_orders_table_usage_is_enabled() ) {
                    // HPOS usage is enabled.
                    $order->update_meta_data('_routeapp_route_charge', $fee->get_amount() );
                    $order->save();
                } else {
                    // Traditional CPT-based orders are in use.
                    update_post_meta( $order->get_id(), '_routeapp_route_charge', $fee->get_amount() );
                }
            }
        }
        $quoteId = 'Invalid';
        if ($quote) {
            $quoteDecoded = json_decode($quote);
            $quoteId = $quoteDecoded->id;
        }

        $destination =  array(
            "first_name"		=> $order->get_billing_first_name(),
            "last_name"		    => $order->get_billing_last_name(),
            "street_address1"   => $order->get_shipping_address_1(),
            "street_address2"	=> $order->get_shipping_address_2(),
            "city"			    => $order->get_shipping_city(),
            "province"		    => $order->get_shipping_state(),
            "zip"			    => $order->get_shipping_postcode(),
            "country_code"	    => $order->get_shipping_country(),
        );
        $customerDetails = array(
            "first_name"	=> $order->get_billing_first_name(),
            "last_name"		=> $order->get_billing_last_name(),
            "email"			=> $order->get_billing_email(),
            "phone"			=> $order->get_billing_phone()
        );
        $order_data = $order->get_data();
        $is_shipping_method_allowed = $this->routeapp_is_shipping_method_allowed(false, $order);

        $itemdata = array();

        foreach ($order->get_items() as $item) {
            ## Using WC_Order_Item_Product methods ##
            $product = $item->get_product(); // Get the WC_Product object

            if ($product instanceof WC_Product) {

                $isProductVirtual = (bool)$product->is_virtual();

                $arrItem = array(
                    "delivery_method" => $isProductVirtual ? 'digital' :
                        ($is_shipping_method_allowed ? 'ship_to_home' : 'ship_to_store'),
                    "is_insured" => $isProductVirtual ? false :
                        ($is_shipping_method_allowed ? true : false),
                    "source_product_id" => $product->get_id(),
                    "source_id" => strval($item->get_id()),
                    "sku" => $product->get_sku(),
                    "name" => $product->get_title(),
                    "price" => $product->get_price(),
                    "quantity" => $item->get_quantity(),
                    "image_url" =>  (wp_get_attachment_url($product->get_image_id())) ? wp_get_attachment_url($product->get_image_id()) : null,
                    "upc" => ""// WooCommerce doesn’t provide UPC by default
                );

                array_push($itemdata, $arrItem);
            }

        }

        if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
            && OrderUtil::custom_orders_table_usage_is_enabled() ) {
            // HPOS usage is enabled.
            $order->update_meta_data('_routeapp_route_protection', $insurance_selected );
            $order->update_meta_data('_routeapp_version', ROUTEAPP_VERSION );

            $order->save();
        } else {
            // Traditional CPT-based orders are in use.
            update_post_meta( $order->get_id(), '_routeapp_route_protection', $insurance_selected );
            update_post_meta( $order->get_id(), '_routeapp_version', ROUTEAPP_VERSION );
        }

        $orderData = array(
            'quote_id' => $quoteId,
            'source_order_id' => $order->get_id(),
            'source_order_number' => $order->get_id(),
            'subtotal' => $order->get_subtotal(),
            'order_total' => $order->get_total(),
            'shipping_total' => $order->get_shipping_total(),
            'discounts_total' => $order->get_discount_total(),
            'source_created_on' => date(DATE_ATOM, strtotime($order->get_date_created())),
            'source_updated_on' => date(DATE_ATOM, strtotime($order->get_date_modified())),
            'taxes' => $order_data['total_tax'],
            'insurance_selected' => $insurance_selected,
            'customer_details' => $customerDetails,
            'shipping_details' => $destination,
            'line_items' => $itemdata,
            'currency' => $order->get_currency(),
            'amount_covered' => $this->get_order_subtotal($order),
            'origin' => 'woocommerce-module',
        );
        //if customer covers the order, pass the paid to insure with Route Fee value
        if ($insurance_selected && $insurance_amount) {
            $orderData['paid_to_insure'] = $insurance_amount;
        }

        return $orderData;
    }

    /**
     * Update tracking order data sending for API
     * @since    1.0.0
     * @param  integer $item_id
     * @param  object $order
     * @param  string $post
     */
    public function routeapp_update_tracking_order_api($item_id, $order, $post) {
        if (is_null($order) || get_post_type($order) !== 'shop_order' || $post == '_edit_lock') {
            return false;
        }
        $this->routeapp_shipment_tracking->update($order, $this);
    }

    /**
     * Update tracking order data sending when HPOS is enable
     * @param  object $order
     * @param  object $data_store
     */
    public function routeapp_update_tracking_order_api_HPOS($order, $data_store) {
        static $already_executed = false;

        if ($already_executed) {
            return false; // Prevent recursion
        }

        $already_executed = true;

        // Ensure the $order object is valid and of type 'shop_order'
        if (!$order || $order->get_type() !== 'shop_order') {
            return false;
        }

        $this->routeapp_shipment_tracking->update($order->get_id(), $this);
    }

    /**
     * Update tracking order item data sending for API
     * @since    1.0.0
     * @param  integer $order_id
     * @param  object $post
     * @param  boolean $update
     */
    public function routeapp_update_tracking_order_item_api($item_id, $order_item_id, $post) {
        if (is_null($order_item_id) ||
            !is_admin() ||
            $this->_isShippingTrackingCall($post)
        ) {
            return false;
        }
        $this->routeapp_shipment_tracking->update($order_item_id, $this);
    }

    /**
     * Check if the post comes for shipment tracking
     * @param $post
     * @return bool
     */
    private function _isShippingTrackingCall($post) {
        $isNotShippingTracking = $post != '_wc_shipment_tracking_items' &&
            $post != '_bst_tracking_number' &&
            $post != 'mimo_tracking_number' &&
            $post != '_aftership_tracking_number' &&
            $post != 'ywot_tracking_code' &&
            $post != 'ph_shipment_tracking_shipping_service' &&
            $post != 'ph_shipment_tracking_ids' &&
            $post != 'usps_evs_label_details_array' &&
            $post != '_order_trackno' &&
            $post != '_order_trackno1' &&
            $post != '_order_trackno2' &&
            $post != '_order_trackno3' &&
            $post != '_order_trackno4' &&
            $post != '_vi_wot_order_item_tracking_data' &&
            $post != 'wc_connect_labels';

        return apply_filters('routeapp_meta_key_is_not_shipping_related', $isNotShippingTracking, $post);
    }


    public function routeapp_update_merchant_api( $merchant_id, $merchantData ) {
        if ( is_null($merchant_id) ) {
            return false;
        }

        $response = $this->routeapp_api_client->update_merchant( $merchant_id, $merchantData );
        if ( is_wp_error($response) ) {
            return false;
        }

        return true;
    }


    /**
     * Updates Route-related metadata to the order after it is placed in draft status.
     *
     * This method is triggered when an order is created in draft status and is responsible for
     * adding meta fields with the list Route sync status.
     * This prevents incorrect synchronization of draft orders.
     *
     * @param int $order_id The ID of the WooCommerce order.
     */
    public function routeapp_save_order_status_to_order( $order_id ) {
        if ( ! $order_id || ! WC()->session ) {
            return;
        }

        try {
            $order = wc_get_order( $order_id );

            if ( ! $order ) {
                throw new Exception( "Order with ID {$order_id} could not be retrieved." );
            }
	        $is_hpos = class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
	                   && OrderUtil::custom_orders_table_usage_is_enabled();

	        $acceptedOrderStatuses = get_option('routeapp_included_order_statuses') ? get_option('routeapp_included_order_statuses') : [];
	        $acceptedCanceledStatuses = get_option('routeapp_cancel_order_statuses') ? get_option('routeapp_cancel_order_statuses') : [];

	        $updated = false;
	        $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_included_order_statuses', $acceptedOrderStatuses );
	        $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_cancel_order_statuses', $acceptedCanceledStatuses );

	        if ($updated && $is_hpos) {
		        $order->save();
	        }
        } catch ( Exception $e ) {
            error_log( "An error occurred while updating Route order status meta fields for order ID {$order_id}: " . $e->getMessage() );
        }
    }


    /**
     * Save into order meta_data the quote from the last request made on session
     *
     * @param $order_id
     */
    public function routeapp_save_quote_to_order($order_id )
    {
        if (!$order_id && !WC()->session) {
            return;
        }
        $order = wc_get_order($order_id);
	    if (!$order) {
		    return;
	    }
	    //MSS-6248, Support High-Performance Order Storage (HPOS); fall back to legacy order storage when disabled.
	    $is_hpos = class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
	               && OrderUtil::custom_orders_table_usage_is_enabled();

	    $updated = false;

        if ($is_hpos) {
            $quote = $order->get_meta('_routeapp_quote');
        } else {
            // Traditional CPT-based orders are in use.
            $quote = get_post_meta($order->get_id(), '_routeapp_quote', true);
        }

		if (!$quote) {
            $cartRef = $order->get_cart_hash();
            if (!$cartRef) {
                //create uniq ID for backend orders
                $cartRef = uniqid();
            }
            $main_key = $this->routeapp_api_client->get_cache_api_session_key().'-'.$cartRef;
            $key = $main_key.'-latest';
            if (WC()->session) {
                $cached = WC()->session->get($key);
                if (isset($cached) && is_array($cached) && isset($cached['body'])) {

                    $fees = $order->get_items('fee');
                    if (!empty($fees)) {
                        foreach ($fees as $fee) {
                            if (self::routeapp_is_route_protection_fee($fee)) {
                                $insurance_amount = $fee->get_amount();
	                            $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_charge', $insurance_amount );
	                            $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_protection', true );
                            }
                        }
                    } else {
                        try {
                            // Check if WC()->cart exists and is accessible
                            if (class_exists('WC_Cart') && isset(WC()->cart) && WC()->cart instanceof WC_Cart) {
                                // Fallback to WC()->cart->get_fees() if no fees are present in the order
                                $cart_fees = WC()->cart->get_fees();
                                if (!empty($cart_fees)) {
                                    foreach ($cart_fees as $cart_fee) {
                                        if (self::routeapp_is_route_protection_fee_name($cart_fee->name)) {
                                            $insurance_amount = $cart_fee->amount; // Correct method for cart fee amount
	                                        $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_charge', $insurance_amount );
	                                        $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_protection', true );
                                        }
                                    }
                                }
                            }
                        } catch (Exception $e) {
                            // Log the exception and ensure the application flow continues
                            error_log("An error occurred while processing cart fees: " . $e->getMessage());
                        }

                    }
	                $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_quote', $cached['body'] );

                    //clear cache!
                    WC()->session->__unset($main_key);
                    WC()->session->__unset($key);
                }
            }
        }

		//Add orders status meta data
	    $acceptedOrderStatuses = get_option('routeapp_included_order_statuses') ? get_option('routeapp_included_order_statuses') : [];
	    $acceptedCanceledStatuses = get_option('routeapp_cancel_order_statuses') ? get_option('routeapp_cancel_order_statuses') : [];

	    $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_included_order_statuses', $acceptedOrderStatuses );
	    $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_cancel_order_statuses', $acceptedCanceledStatuses );

		//MSS-6533: Add ship to store flag and excluded shipping methods meta data to the order
	    $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos,'_routeapp_order_ship_to_store', $this->routeapp_resolve_ship_to_store_flag( $order ) );
	    $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_excluded_shipping_methods', $this->routeapp_get_excluded_shipping_methods());

	    // Save order if there is any update
	    if ($updated && $is_hpos) {
		    $order->save();
	    }
    }

    /**
     * Mark order on woocommerce thank you hook
     * @since    1.0.0
     * @param  integer $order_id
     *
     */
    public function routeapp_mark_order($order_id)
    {
        if (!$order_id) {
            return;
        }

        $order = wc_get_order($order_id);

        //order cancel action will be handled by webhook event
        if ($order->get_status() === 'cancelled') {
            return;
        }

        // Init the setup for readiness report
        $this->_setup_readiness_report_check();

        if (WC()->session) {
            WC()->session->__unset('checkbox_checked');
        }
    }

    /**
     * Check order note (look for shipment tracking data on it)
     * @param $noteId id
     * @return mixed
     */
    public function check_order_note_by_note_id($noteId)
    {
        $comment = get_comment($noteId);
        $order = wc_get_order($comment->comment_post_ID);
        if ($order && $order->get_id()) {
            $this->routeapp_shipment_tracking->parse_order_notes($order->get_id(), $this);
        }
    }

    /**
     * Check order note (look for shipment tracking data on it)
     * @param $response WP_REST_Response
     * @return mixed
     */
    public function check_order_note($response)
    {
        $note = $response->get_data();
        $comment = get_comment($note['id']);
        $order = wc_get_order($comment->comment_post_ID);
        if ($order && $order->get_id()) {
            $this->routeapp_shipment_tracking->parse_order_notes($order->get_id(), $this);
        }
        return $response;
    }
    /**
     * Cancel the tracking order sending for API
     * @since    1.0.0
     * @param  integer $order_id
     * @param  integer $tracking_number
     * @param  array $product_ids
     */

    public function routeapp_cancel_tracking_order( $order_id, $tracking_number, $product_ids ) {
        if ( is_null($order_id) ) {
            return false;
        }
        $this->routeapp_shipment_tracking->cancel($order_id, $tracking_number, $product_ids, $this);
    }

    /**
     * Create user at Route
     * @param $data
     * @return array|bool|mixed|object
     */
    public function routeapp_create_user($data){
        $response = $this->routeapp_api_client->create_user($data);

        $this->routeapp_last_request = $response;

        if ( is_wp_error( $response ) ) {
            return false;
        } else {
            $body = json_decode($response['body']);

            if ($response['response']['code'] != self::HTTP_CREATED_CODE) {
                return false;
            }

            return $body;
        }
    }

    /**
     * Create user at Route
     *
     * @param $username
     * @param $password
     *
     * @return array|bool|mixed|object
     */
    public function routeapp_user_login($username, $password){
        $response = $this->routeapp_api_client->login_user($username, $password);

        $this->routeapp_last_request = $response;

        if ( is_wp_error( $response ) ) {
            return false;
        } else {
            $body = json_decode($response['body']);

            if ($response['response']['code'] != self::HTTP_SUCCESS_CODE) {
                return false;
            }

            return $body;
        }
    }

    /**
     * Creates merchant at Route
     *
     * @param $data
     * @return array|bool|mixed|object
     */
    public function routeapp_create_merchant($data){

        $response = $this->routeapp_api_client->create_merchant($data);
        $this->routeapp_last_request = $response;

        if ( is_wp_error( $response ) ) {
            return false;
        } else {
            $body = json_decode($response['body']);

            if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
                return false;
            }

            return $body;
        }
    }

    /**
     * Creates merchant at Route
     *
     * @param $data
     * @return array|bool|mixed|object
     */
    public function routeapp_get_merchants(){

        $response = $this->routeapp_api_client->get_merchants();
        $this->routeapp_last_request = $response;

        if ( is_wp_error( $response ) ) {
            return false;
        } else {
            $body = json_decode($response['body']);

            if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
                return false;
            }

            return $body;
        }
    }

    /**
     * Activate user account
     *
     * @param $email
     *
     * @return array|bool|mixed|object
     */
    public function routeapp_activate_account($email){

        $response = $this->routeapp_api_client->activate_account(['email' => $email]);
        $this->routeapp_last_request = $response;

        if ( is_wp_error( $response ) ) {
            return false;
        } else {
            $body = json_decode($response['body']);

            if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
                return false;
            }

            return $body;
        }
    }

    public function routeapp_is_shipping_method_allowed($shipping_method=false, $order=false) {

        if ($shipping_method || $order) {
            if ($shipping_method) {
                $shipping_method = explode(':', $shipping_method);
                $shipping_method = $shipping_method[0];
            }
            if ($order) {
                $shipping_method = @array_shift($order->get_shipping_methods());
                if(isset($shipping_method['method_id']))
                {
                    $shipping_method = $shipping_method['method_id'];
                } else {
                    return false;
                }
            }
            if ($this->routeapp_is_shipping_method_excluded($shipping_method)) {
                return false;
            }
        } else {
            if (WC()->session->get('shipping_for_package_0')) {
                foreach( WC()->session->get('shipping_for_package_0')['rates'] as $method_id => $rate ){
                    if( WC()->session->get('chosen_shipping_methods')[0] == $method_id ){
                        $shippingMethod = explode(':', $rate->id);
                        $shippingMethod = $shippingMethod[0];
                        if ($this->routeapp_is_shipping_method_excluded($shippingMethod)) {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }

    /**
     * Whether a shipping method is configured as in-store pickup (excluded from protection).
     * WooCommerce Blocks local pickup uses pickup_location; classic checkout uses local_pickup.
     *
     * @param string $shipping_method Method id, optionally with instance suffix (e.g. pickup_location:0).
     * @return bool
     */
    public function routeapp_is_shipping_method_excluded($shipping_method) {
        $shipping_method = explode(':', (string) $shipping_method)[0];
        $excluded = $this->routeapp_get_excluded_shipping_methods();
        if (!is_array($excluded) || empty($excluded) || $shipping_method === '') {
            return false;
        }
        if (in_array($shipping_method, $excluded, true)) {
            return true;
        }
        if (in_array($shipping_method, self::PICKUP_SHIPPING_METHOD_IDS, true)) {
            return (bool) array_intersect(self::PICKUP_SHIPPING_METHOD_IDS, $excluded);
        }
        return false;
    }

    public function routeapp_get_insurance_label() {
        return __(self::ROUTE_LABEL, 'routeapp');
    }

    public function routeapp_is_opt_in() {
        $merchant = $this->routeapp_api_client->get_merchant();
        return isset($merchant->merchant_preferences->opt_in) && $merchant->merchant_preferences->opt_in == true;
    }

    public function routeapp_get_public_token() {
        return get_option('routeapp_public_token');
    }

    public static function routeapp_get_taxable_class() {
        return get_option('routeapp_taxable_class');
    }

    public static function routeapp_get_checkout_hook() {
        return get_option('routeapp_checkout_hook') ? get_option('routeapp_checkout_hook') : self::DEFAULT_CHECKOUT_WEBHOOK;
    }

    public static function routeapp_get_checkout_hook_options() {
        return [
            'woocommerce_before_checkout_form' => 'woocommerce_before_checkout_form',
            'woocommerce_checkout_before_customer_details' => 'woocommerce_checkout_before_customer_details',
            'woocommerce_checkout_billing' => 'woocommerce_checkout_billing',
            'woocommerce_before_checkout_billing_form' => 'woocommerce_before_checkout_billing_form',
            'woocommerce_after_checkout_billing_form' => 'woocommerce_after_checkout_billing_form',
            'woocommerce_before_checkout_registration_form' => 'woocommerce_before_checkout_registration_form',
            'woocommerce_after_checkout_registration_form' => 'woocommerce_after_checkout_registration_form',
            'woocommerce_checkout_shipping' => 'woocommerce_checkout_shipping',
            'woocommerce_before_checkout_shipping_form' => 'woocommerce_before_checkout_shipping_form',
            'woocommerce_after_checkout_shipping_form' => 'woocommerce_after_checkout_shipping_form',
            'woocommerce_before_order_notes' => 'woocommerce_before_order_notes',
            'woocommerce_after_order_notes' => 'woocommerce_after_order_notes',
            'woocommerce_checkout_after_customer_details' => 'woocommerce_checkout_after_customer_details',
            'woocommerce_checkout_before_order_review_heading' => 'woocommerce_checkout_before_order_review_heading',
            'woocommerce_checkout_order_review' => 'woocommerce_checkout_order_review',
            'woocommerce_checkout_before_order_review' => 'woocommerce_checkout_before_order_review',
            'woocommerce_review_order_before_cart_contents' => 'woocommerce_review_order_before_cart_contents',
            'woocommerce_review_order_after_cart_contents' => 'woocommerce_review_order_after_cart_contents',
            'woocommerce_review_order_before_shipping' => 'woocommerce_review_order_before_shipping',
            'woocommerce_review_order_after_shipping' => 'woocommerce_review_order_after_shipping',
            'woocommerce_review_order_before_order_total' => 'woocommerce_review_order_before_order_total',
            'woocommerce_review_order_after_order_total' => 'woocommerce_review_order_after_order_total',
            'woocommerce_review_order_before_payment' => 'woocommerce_review_order_before_payment',
            'woocommerce_review_order_before_submit' => 'woocommerce_review_order_before_submit',
            'woocommerce_review_order_after_submit' => 'woocommerce_review_order_after_submit',
            'woocommerce_review_order_after_payment' => 'woocommerce_review_order_after_payment',
            'woocommerce_checkout_after_order_review' => 'woocommerce_checkout_after_order_review',
            'woocommerce_after_checkout_form' => 'woocommerce_after_checkout_form'
        ];
    }

    public static  function routeapp_get_fee_taxable() {
        if (get_option('routeapp_route_fee_taxable') == 'yes') return true;
        return false;
    }

    public function routeapp_get_excluded_shipping_methods() {
        return get_option('routeapp_excluded_shipping_methods') ?
            get_option('routeapp_excluded_shipping_methods') :
            [];
    }

    public function routeapp_get_secret_token() {
        return get_option('routeapp_secret_token');
    }

    public function routeapp_route_enabled_extra_columns() {
        return get_option('routeapp_route_enable_extra_columns') !== "no";
    }

    public function routeapp_get_fresh_installed(){
        return get_option('routeapp_route_fresh_installed');
    }

    public function routeapp_route_show_order_updates() {
        return get_option('routeapp_route_show_order_updates', 'yes') == 'yes';
    }

    private static function routeapp_get_failed_registration(){
        return get_option('routeapp_failed_registration');
    }

    /**
     * If it's installation has failed at user step creation step
     *
     * @return bool
     */
    public function has_user_creation_failed(){
        return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_USER;
    }

    /**
     * If it's installation has failed at user step creation step due to conflict
     *
     * @return bool
     */
    public function has_user_creation_conflicted(){
        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);
    }

    /**
     * If it's installation has failed at Merchant creation step
     *
     * @return bool
     */
    public function has_merchant_creation_failed(){
        return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_MERCHANT  && !Route_Setup::has_valid_merchant_tokens();
    }
    /**
     * If it's installation has failed at Merchant creation step
     *
     * @return bool
     */
    public function has_merchant_creation_conflicted(){
        return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_MERCHANT_DUPLICATED && !Route_Setup::has_valid_merchant_tokens();
    }

    /**
     * If it's installation has failed at Merchant creation step
     *
     * @return bool
     */
    public function successful_registration(){
        return self::routeapp_get_failed_registration() === Route_Setup::REGISTRATION_FIXED || self::routeapp_get_failed_registration() === Route_Setup::REGISTRATION_STEP_USER_LOGIN_SUCCESS;
    }

    /**
     * Get registration step at setup flow
     * @return mixed
     */
    private static function registration_step(){
        return get_option(Route_Setup::REGISTRATION_STEP);
    }

    /**
     * Check if at the merchant create flow it was already redirected
     * @return bool
     */
    public static function was_redirected(){
        return self::registration_step() == Route_Setup::REDIRECTED;
    }

    /**
     * Set it as redirected to Route after registration is complete
     * @return void
     */
    public function set_as_redirected(){
        update_option(Route_Setup::REGISTRATION_STEP, Route_Setup::REDIRECTED);
    }

    public function routeapp_register_status_route() {
        register_rest_route( 'route', 'status', array(
                'methods' => 'GET',
                'callback' => function() {
                    return rest_ensure_response( $this->routeapp_build_status_response() );
                },
                'permission_callback' => '__return_true'
            )
        );
    }

    /**
     * Build the associative array returned by GET /wp-json/route/status.
     *
     * Kept separate from the REST callback so tests can exercise the response
     * shape directly without REST dispatch. Never returns raw credentials —
     * `routeapp_user_token` and `routeapp_user_id` are intentionally excluded;
     * `public_token`, `secret_key`, and `routeapp_store_hash` are masked via
     * {@see Routeapp_Public::_encodeToken()}. `routeapp_merchant_id` is
     * exposed raw — it is the merchant identifier Route support matches
     * against, and already appears in outbound webhook URLs.
     *
     * @return array Flat associative array keyed by setting name.
     */
    private function routeapp_build_status_response() {
        $store_hash = Route_Setup::get_store_hash();
        $widget_install_ts = get_option( Route_Setup::SETUP_CHECK_WIDGET_INSTALLATION_DATE, null );
        $widget_install_human = null;
        if ( ! empty( $widget_install_ts ) && is_numeric( $widget_install_ts ) ) {
            $widget_install_ts = (int) $widget_install_ts;
            $widget_install_human = wp_date( 'Y-m-d H:i:s', $widget_install_ts );
        } else {
            $widget_install_ts = null;
        }

        return array(
            'version' => ROUTEAPP_VERSION,
            'public_token' => !empty($this->routeapp_get_public_token()) ?
                $this->_encodeToken($this->routeapp_get_public_token()) :
                false,
            'secret_key' => !empty($this->routeapp_get_secret_token()) ?
                $this->_encodeToken($this->routeapp_get_secret_token()) :
                false,
            'route_insurance_label' => $this->routeapp_get_insurance_label(),
            'extra_columns_enabled' => $this->routeapp_route_enabled_extra_columns(),
            'failed_registration' => $this->routeapp_get_failed_registration(),
            'base_currency' => get_woocommerce_currency(),
            'date' => array(
                'date' => date('Y-m-d H:i:s'),
                'timezone_type' => date('P'),
                'timezone' => date_default_timezone_get()
            ),
            'routeapp_route_fee_taxable' => $this->routeapp_get_fee_taxable(),
            'routeapp_taxable_class' => $this->routeapp_get_taxable_class(),
            'routeapp_excluded_shipping_methods' => $this->routeapp_get_excluded_shipping_methods(),
            'routeapp_route_show_order_updates' => $this->routeapp_route_show_order_updates(),
            'routeapp_checkout_hook' => $this->routeapp_get_checkout_hook(),
            'routeapp_have_webhook_secret' => $this->_routeapp_have_webhook_secret(),
            'routeapp_included_order_statuses' => get_option( 'routeapp_included_order_statuses', array() ),
            'routeapp_cancel_order_statuses' => get_option( 'routeapp_cancel_order_statuses', array() ),
            'routeapp_external_tracking_providers' => get_option( 'routeapp_external_tracking_providers', '' ),
            'routeapp_order_recover_from' => get_option( 'routeapp_order_recover_from', '' ),
            'routeapp_order_recover_to' => get_option( 'routeapp_order_recover_to', '' ),
            'routeapp_order_recover_reconcile_backend' => get_option( 'routeapp_order_recover_reconcile_backend', 'no' ),
            'routeapp_merchant_id' => get_option( Routeapp_API_Client::ROUTEAPP_MERCHANT_ID, '' ),
            'routeapp_store_hash' => ! empty( $store_hash ) ? $this->_encodeToken( $store_hash ) : false,
            'routeapp_last_install_date' => get_option( '_routeapp_last_install_date', '' ),
            'routeapp_webhooks_created' => (bool) get_option( '_routeapp_webhooks_created', false ),
            'routeapp_route_registration_step' => get_option( Route_Setup::REGISTRATION_STEP, '' ),
            'routeapp_user_conflict_auto_redirect_done' => (bool) get_option( Route_Setup::USER_CONFLICT_AUTO_REDIRECT_DONE_OPTION, false ),
            'routeapp_route_fresh_installed' => $this->routeapp_get_fresh_installed(),
            'route_setup_check_widget_installation_date' => $widget_install_ts,
            'route_setup_check_widget_installation_date_human' => $widget_install_human,
            'route_setup_check_widget_php' => (bool) get_option( Route_Setup::SETUP_CHECK_WIDGET_PHP, false ),
            'route_setup_check_widget_js' => (bool) get_option( Route_Setup::SETUP_CHECK_WIDGET_JS, false ),
            'route_setup_check_widget_api_call' => (bool) get_option( Route_Setup::SETUP_CHECK_WIDGET_API_CALL, false ),
        );
    }

    private function _routeapp_have_webhook_secret() {
        return get_option('_routeapp_webhooks_secret') ? 'Yes' : 'No';
    }

    private function _encodeToken($token) {
        return substr($token, 0, 5) . '...' . substr($token, -5);
    }

    public function routeapp_register_checkout_route(){

        if (!isset(WC()->session) || !isset($_POST) || !isset($_POST['shipping_method'])) {
            return rest_ensure_response( [] );
        }

        $shipping_method = $_POST['shipping_method'];
        if ($shipping_method === 'false' || $shipping_method === '') {
            $shipping_method = false;
        }

        //check first if shipping method is allowed
        if ($shipping_method) {
            $show_widget =  $this->routeapp_is_shipping_method_allowed($shipping_method);
            if (!$show_widget) {
                wp_send_json(['hide-widget' => true]);
                return;
            }
        }

        $cart = WC()->cart;
        $response= [
            'routeapp-subtotal' => $cart ? round($this->get_cart_subtotal_with_only_shippable_items($cart), 2) : 0,
        ];

        wp_send_json( $response );
    }

    /**
     * Add route to recreate User and Merchant Accounts
     * if it has failed at the first try
     */
    public function routeapp_register_setup_route () {
        register_rest_route( 'route', 'recreate_user', array(
            'methods' => 'GET',
            'callback' => [Route_Setup::class, 'retry_user'],
            'permission_callback' => '__return_true'
        ) );
        register_rest_route( 'route', 'recreate_merchant', array(
            'methods' => 'GET',
            'callback' => [Route_Setup::class, 'retry_merchant'],
            'permission_callback' => '__return_true'
        ) );
        register_rest_route( 'route', 'user_login', array(
            'methods' => 'POST',
            'callback' => [Route_Setup::class, 'user_login'],
            'permission_callback' => '__return_true'
        ) );
    }

    public function routeapp_last_request_has_failed(){
        if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
            return true;
        } else {
            if ($this->routeapp_last_request['response']['code'] != self::HTTP_CREATED_CODE && $this->routeapp_last_request['response']['code'] != self::HTTP_SUCCESS_CODE) {
                return true;
            }

            return false;
        }
    }

    public function routeapp_last_request_has_success() {
        if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
            return false;
        }
        return $this->routeapp_last_request['response']['code'] === self::HTTP_SUCCESS_CODE;
    }

    public function routeapp_last_request_has_conflicted(){
        if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
            return false;
        } else {
            if ($this->routeapp_last_request['response']['code'] == self::HTTP_CONFLICT_CODE) {
                return true;
            }

            return false;
        }
    }

    public function routeapp_log($event, $extraData = false) {
        $this->routeapp_logger->sentry_log($event, $extraData);
    }

    private function _setup_readiness_report_check()
    {
        if ($this->route_app_setup_helper->can_send_compatibility_report()) {
            /**
             * API CALL for incompatibility test
             */
            $api = new Routeapp_API_Compatibility();
            $api->send($this->route_app_setup_helper->prepare_compatibility_data());
        }
    }

    public function get_order_subtotal($order)
    {
        $subtotal = 0;

        if (is_a($order, 'WC_Order')) {
            $order_items = $order->get_items();

            foreach ($order_items as $order_item_id => $order_item) {
                if ($this->isProductVirtual($order_item)) {
                    //if the product is not found or virtual, skip it
                    continue;
                }
                $subtotal += $order_item->get_subtotal();
            }
        }
        return $subtotal;
    }

    /**
     * Normalize cart item (array) or order item (WC_Order_Item_Product) to a common array format.
     * Ensures compatibility with WooCommerce Product Bundles when used in order context.
     *
     * @param array|WC_Order_Item_Product $item Cart item array or order item object.
     * @return array Normalized item with keys: data, line_subtotal, quantity, product_id, variation_id, key, bundled_items, bundled_by.
     */
    private function normalize_item_for_bundle_calc($item) {
        if (is_object($item) && method_exists($item, 'get_product')) {
            /** @var WC_Order_Item_Product $item */
            $product = $item->get_product();
            $bundled_items = $item->get_meta('_bundled_items', true);
            $bundled_by = $item->get_meta('_bundled_by', true);
            // Use _bundle_cart_key so parent matches child's _bundled_by (Product Bundles uses same hash).
            $key = $item->get_meta('_bundle_cart_key', true) ?: $item->get_id();
            return [
                'data' => $product,
                'line_subtotal' => (float) $item->get_subtotal(),
                'quantity' => (int) $item->get_quantity(),
                'product_id' => $item->get_product_id(),
                'variation_id' => $item->get_variation_id(),
                'key' => $key,
                'bundled_items' => !empty($bundled_items) ? $bundled_items : null,
                'bundled_by' => !empty($bundled_by) ? $bundled_by : null,
            ];
        }
        return $item;
    }

    /**
     * Check if product inside order item is virtual, return true if product is not valid
     * @param array|WC_Order_Item_Product $item Cart item array or order item object.
     * @return bool
     */
    public function isProductVirtual($item) {
        if (is_object($item) && method_exists($item, 'get_product_id')) {
            $productId = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id();
        } else {
            $productId = isset($item['variation_id']) && !empty($item['variation_id']) ?
                $item['variation_id'] :
                $item['product_id'];
        }

        $product = wc_get_product($productId);
        if ($product) {
            return $product->is_virtual();
        }
        return true;
    }

    /**
     * Process cart item
     *
     * @param $cart_item
     * @param $allowed_bundle_parents
     * @return array
     */
    public function processCartItem($cart_item, $allowed_bundle_parents) {

        $cart_item = $this->normalize_item_for_bundle_calc($cart_item);

        $cart_item_subtotal = $cart_item['line_subtotal'];

        //support for WooCommerce Product Bundles (only when product data is available; order items may have null)
        if (isset($cart_item['bundled_items'])) {
            if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
                $price = $cart_item['data']->get_data()['price'];
                if ($price > 0) {
                    //grab item price from parent bundle if exist, return directly
                    $cart_item_subtotal = $price * $cart_item['quantity'];
                    $data = [
                        'id' => strval($cart_item['product_id']),
                        'quantity' => intval($cart_item['quantity']),
                        'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2))
                    ];
                    return ['allowed_bundle_parents' => $allowed_bundle_parents, 'data' => $data];
                }
            }
            //add bundle parent key to allowed array
            $allowed_bundle_parents[] = $cart_item['key'];
        }

        if ($this->isProductVirtual($cart_item)) {
            //if the product is not found or virtual, skip it
            return ['allowed_bundle_parents' => $allowed_bundle_parents];
        }

        if (isset($cart_item['bundled_by'])) {
            if (!in_array($cart_item['bundled_by'], $allowed_bundle_parents)) {
                //skip bundle item children (price is on parent)
                return ['allowed_bundle_parents' => $allowed_bundle_parents];
            }
            if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
                //grab subtotal from bundle child
                $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
            }
        }

        return  [
            'allowed_bundle_parents' => $allowed_bundle_parents,
            'data' => [
                'id' => strval($cart_item['product_id']),
                'quantity' => intval($cart_item['quantity']),
                'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2))
            ]
        ];
    }

    /**
     * Get cart item subtotal
     *
     * @param $cart_item
     * @param $allowed_bundle_parents
     * @return array
     */
    public function getCartItemSubtotal($cart_item, $allowed_bundle_parents) {

        $cart_item = $this->normalize_item_for_bundle_calc($cart_item);

        $cart_item_subtotal = $cart_item['line_subtotal'];

        //support for WooCommerce Product Bundles (only when product data is available; order items may have null)
        if (isset($cart_item['bundled_items'])) {
            if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
                $price = $cart_item['data']->get_data()['price'];
                if ($price > 0) {
                    //grab item price from parent bundle if exist, return directly
                    $cart_item_subtotal = $price * $cart_item['quantity'];
                    return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal];
                }
            }
            //add bundle parent key to allowed array
            $allowed_bundle_parents[] = $cart_item['key'];
        }

        if ($this->isProductVirtual($cart_item)) {
            //if the product is not found or virtual, skip it
            return ['allowed_bundle_parents' => $allowed_bundle_parents];
        }

        if (isset($cart_item['bundled_by'])) {
            if (!in_array($cart_item['bundled_by'], $allowed_bundle_parents)) {
                //skip bundle item children (price is on parent)
                return ['allowed_bundle_parents' => $allowed_bundle_parents];
            }
            if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
                //grab subtotal from bundle child
                $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
            }
        }
        return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal];
    }

    /**
     * get cart shippable items to grab quote on RouteApp
     *
     * @param $cart
     * @param string $object
     * @return array
     */
    public function get_cart_shippable_items($cart, $object='cart')
    {
        $cartItems = [];
        if (!$cart) {
            return $cartItems;
        }
        $allowed_bundle_parents = [];
        if ($object=='cart') {
            foreach ( $cart->get_cart() as $cart_item ) {
                $cartItemData = $this->processCartItem($cart_item, $allowed_bundle_parents);
                $allowed_bundle_parents = $cartItemData['allowed_bundle_parents'];
                if (isset($cartItemData['data'])) {
                    $cartItems[] = $cartItemData['data'];
                }
            }
        } else {
            foreach ( $cart->get_items() as $cart_item ) {
                $cartItemData = $this->processCartItem($cart_item, $allowed_bundle_parents);
                $allowed_bundle_parents = $cartItemData['allowed_bundle_parents'];
                if (isset($cartItemData['data'])) {
                    $cartItems[] = $cartItemData['data'];
                }
            }
        }

        return $cartItems;
    }

    /**
     * get subtotal for shippable items only
     *
     * @param $cart
     * @param string $object
     * @return float|int|mixed
     */
    public function get_cart_subtotal_with_only_shippable_items($cart, $object='cart')
    {
        $subtotal = 0;
        if (!$cart) {
            return $subtotal;
        }
        $allowed_bundle_parents = [];

        if ($object=='cart') {
            foreach ( $cart->get_cart() as $cart_item ) {
                $itemSubtotal = $this->getCartItemSubtotal($cart_item, $allowed_bundle_parents);
                $allowed_bundle_parents = $itemSubtotal['allowed_bundle_parents'];
                if (isset($itemSubtotal['subtotal'])) {
                    $subtotal += $itemSubtotal['subtotal'];
                }
            }
        } else {
            foreach ( $cart->get_items() as $cart_item ) {
                $itemSubtotal = $this->getCartItemSubtotal($cart_item, $allowed_bundle_parents);
                $allowed_bundle_parents = $itemSubtotal['allowed_bundle_parents'];
                if (isset($itemSubtotal['subtotal'])) {
                    $subtotal += $itemSubtotal['subtotal'];
                }
            }
        }
        return $subtotal;
    }

    /**
     * Edit Avatax fee line (send tax class name instead item_id)
     */
    public function routeapp_edit_avatax_fee($fee_line, $fee = null)
    {
        if (isset($fee_line['description']) && $fee_line['description'] == $this->routeapp_get_insurance_label()) {
            $fee_line['itemCode'] = $this->routeapp_get_taxable_class();
        }

        return $fee_line;
    }

    /**
     * Mark a fee order item as Route Shipping Protection.
     *
     * Writes fee item meta:
     * - _routeapp_protection_fee = yes
     * - _routeapp_protection_sku = ROUTEINS
     *
     * Purpose: leave a stable identity on the WC_Order_Item_Fee that does not
     * depend on the (possibly translated) fee name. Called when the fee is
     * created at checkout and from the admin order fee path.
     *
     * @param mixed $item Fee order item.
     */
    public static function routeapp_mark_protection_fee_item( $item ) {
        if ( ! is_object( $item ) || ! is_callable( array( $item, 'update_meta_data' ) ) ) {
            return;
        }

        $item->update_meta_data( self::ROUTE_FEE_ITEM_META, 'yes' );
        $item->update_meta_data( self::ROUTE_FEE_SKU_META, self::ROUTE_PROTECTION_SKU );
    }

    /**
     * Whether a fee name refers to Route Shipping Protection (EN or translated).
     *
     * Name matching is a legacy fallback only. New fees are identified via
     * ROUTE_FEE_ITEM_META / ROUTE_FEE_SKU_META. Besides the English constant and
     * the current-locale translation, we also resolve the store locale label when
     * it differs from the request locale (e.g. admin export vs checkout).
     *
     * Hot path: prefer strict string matches before html_entity_decode().
     *
     * @param string $name Fee name.
     * @return bool
     */
    public static function routeapp_is_route_protection_fee_name( $name ) {
        $name = trim( (string) $name );
        if ( '' === $name ) {
            return false;
        }

        // Common case: plain English constant / translated label, no entities.
        if ( $name === self::ROUTE_LABEL ) {
            return true;
        }

        $labels = self::routeapp_get_protection_fee_label_candidates();
        foreach ( $labels as $label ) {
            if ( $name === $label ) {
                return true;
            }
        }

        // Only decode when needed (e.g. "Route Shipping Protection" stored as entities).
        if ( false === strpos( $name, '&' ) ) {
            return false;
        }

        $decoded = trim( html_entity_decode( $name, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
        if ( '' === $decoded || $decoded === $name ) {
            return false;
        }

        if ( $decoded === self::ROUTE_LABEL ) {
            return true;
        }

        foreach ( $labels as $label ) {
            if ( $decoded === $label ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Possible Route fee labels for the current request / store locales.
     *
     * Cached per request to avoid repeated locale switching in fee loops.
     *
     * @return string[]
     */
    private static function routeapp_get_protection_fee_label_candidates() {
        static $cached_labels = null;
        static $cached_locale_key = null;

        $current_locale = function_exists( 'determine_locale' )
            ? determine_locale()
            : ( function_exists( 'get_locale' ) ? get_locale() : '' );
        $site_locale = function_exists( 'get_locale' ) ? get_locale() : '';
        $locale_key  = $current_locale . '|' . $site_locale;

        if ( null !== $cached_labels && $cached_locale_key === $locale_key ) {
            return $cached_labels;
        }

        $labels = array();

        if ( function_exists( '__' ) ) {
            $labels[] = __( self::ROUTE_LABEL, 'routeapp' );

            // When the admin/user locale differs from the site locale, also check the
            // store translation used at checkout for older orders without fee meta.
            if ( function_exists( 'switch_to_locale' )
                && function_exists( 'restore_previous_locale' )
                && $site_locale
                && $site_locale !== $current_locale
                && switch_to_locale( $site_locale ) ) {
                $labels[] = __( self::ROUTE_LABEL, 'routeapp' );
                restore_previous_locale();
            }
        }

        $cached_labels     = array_values( array_unique( array_filter( $labels ) ) );
        $cached_locale_key = $locale_key;

        return $cached_labels;
    }

    /**
     * Whether an order item is the Route Shipping Protection fee.
     *
     * Prefers explicit fee item meta for all newly created fees; falls back to
     * fee name only for legacy pre-migration orders.
     *
     * @param mixed $item Order item.
     * @return bool
     */
    public static function routeapp_is_route_protection_fee( $item ) {
        if ( ! is_object( $item ) ) {
            return false;
        }

        if ( is_callable( array( $item, 'get_type' ) ) && 'fee' !== $item->get_type() ) {
            return false;
        }

        if ( is_callable( array( $item, 'get_meta' ) ) ) {
            $flag = $item->get_meta( self::ROUTE_FEE_ITEM_META, true );
            if ( 'yes' === $flag || true === $flag || '1' === $flag || 1 === $flag ) {
                return true;
            }

            $sku = $item->get_meta( self::ROUTE_FEE_SKU_META, true );
            if ( self::ROUTE_PROTECTION_SKU === $sku ) {
                return true;
            }
        }

        $name = is_callable( array( $item, 'get_name' ) ) ? $item->get_name() : '';
        if ( ! self::routeapp_is_route_protection_fee_name( $name ) ) {
            return false;
        }

        // Legacy path: visible when an older fee lacks identity meta (locale gaps).
        static $logged_name_fallback = false;
        if ( ! $logged_name_fallback && function_exists( 'error_log' ) ) {
            $logged_name_fallback = true;
            error_log(
                'RouteApp: identified Route protection fee via name fallback (legacy order without fee meta). Name: ' . $name
            );
        }

        return true;
    }

    /**
     * Exclude Route fee from ShipStation item export (XML legacy + REST 4.9+).
     *
     * Used by woocommerce_shipstation_no_shipping_item on both export paths.
     *
     * @param bool            $exclude Whether the item should be excluded.
     * @param mixed            $product Product object or false for fees.
     * @param mixed            $item    Order item.
     * @return bool
     */
    public function routeapp_exclude_route_from_shipstation( $exclude, $product, $item ) {
        if ( self::routeapp_is_route_protection_fee( $item ) ) {
            return true;
        }

        return $exclude;
    }

    /**
     * Strip Route fee from ShipStation REST fulfillment item payload (4.8+/4.9+).
     *
     * @param array $fulfillment_item Fulfillment item data.
     * @param mixed $order            Order object.
     * @param mixed $item             Order item.
     * @param array $extra_args       Extra args.
     * @return array
     */
    public function routeapp_strip_route_from_shipstation_fulfillment_item( $fulfillment_item, $order, $item, $extra_args = array() ) {
        if ( self::routeapp_is_route_protection_fee( $item ) ) {
            return array();
        }

        return $fulfillment_item;
    }

    /**
     * Legacy alias kept for backwards compatibility with anything calling the old method name.
     *
     * @param mixed $order_xml Order XML DOM element.
     * @return mixed
     */
    public function routeapp_add_sku_to_route_node( $order_xml ) {
        return $this->routeapp_strip_route_from_shipstation_xml( $order_xml );
    }

    /**
     * Remove Route Shipping Protection from ShipStation XML order export (legacy path).
     *
     * @param mixed $order_xml Order XML DOM element.
     * @return mixed
     */
    public function routeapp_strip_route_from_shipstation_xml( $order_xml ) {
        if ( ! ( $order_xml instanceof DOMElement ) ) {
            return $order_xml;
        }

        $items_nodes = $order_xml->getElementsByTagName( 'Items' );
        foreach ( $items_nodes as $item_node ) {
            $to_remove = array();

            foreach ( $item_node->childNodes as $child ) {
                if ( ! ( $child instanceof DOMElement ) || 'Item' !== $child->nodeName ) {
                    continue;
                }

                if ( $this->routeapp_shipstation_xml_item_is_route( $child ) ) {
                    $to_remove[] = $child;
                }
            }

            foreach ( $to_remove as $node ) {
                $item_node->removeChild( $node );
            }
        }

        return $order_xml;
    }

    /**
     * Detect Route fee nodes inside a ShipStation XML <Item>.
     *
     * @param DOMElement $item Item element.
     * @return bool
     */
    private function routeapp_shipstation_xml_item_is_route( DOMElement $item ) {
        foreach ( $item->childNodes as $child ) {
            if ( ! ( $child instanceof DOMElement ) ) {
                continue;
            }

            if ( 'Name' === $child->nodeName && self::routeapp_is_route_protection_fee_name( $child->nodeValue ) ) {
                return true;
            }

            if ( 'SKU' === $child->nodeName && self::ROUTE_PROTECTION_SKU === trim( $child->nodeValue ) ) {
                return true;
            }

            if ( 'Options' !== $child->nodeName ) {
                continue;
            }

            foreach ( $child->getElementsByTagName( 'Option' ) as $option ) {
                $opt_name  = '';
                $opt_value = '';

                foreach ( $option->childNodes as $opt_child ) {
                    if ( ! ( $opt_child instanceof DOMElement ) ) {
                        continue;
                    }
                    if ( 'Name' === $opt_child->nodeName ) {
                        $opt_name = trim( $opt_child->nodeValue );
                    }
                    if ( 'Value' === $opt_child->nodeName ) {
                        $opt_value = trim( strip_tags( $opt_child->nodeValue ) );
                    }
                }

                if ( self::ROUTE_FEE_ITEM_META === $opt_name && in_array( $opt_value, array( 'yes', '1' ), true ) ) {
                    return true;
                }

                if ( in_array( $opt_name, array( self::ROUTE_FEE_SKU_META, 'SKU' ), true )
                    && self::ROUTE_PROTECTION_SKU === $opt_value ) {
                    return true;
                }
            }
        }

        return false;
    }

	/**
	 * Update order meta only if changed (HPOS & CPT safe)
	 *
	 * @return bool True if meta changed
	 */
	public function updateRouteOrderMetaData(
		WC_Order $order,
		bool $is_hpos,
		string $meta_key,
		$meta_value
	): bool {
		try {
			if ( ! $order || empty( $meta_key ) ) {
				return false;
			}

			$normalized = is_scalar( $meta_value )
				? (string) $meta_value
				: wp_json_encode( $meta_value );

			if ( $is_hpos ) {
				$current = $order->get_meta( $meta_key, true );
				$current = is_scalar( $current ) ? (string) $current : wp_json_encode( $current );


				if ( (string) $current === (string) $normalized ) {
					return false;
				}

				$order->update_meta_data( $meta_key, $normalized );
				return true;

			}

			$order_id = $order->get_id();
			$current = get_post_meta( $order_id, $meta_key, true );
			$current = is_scalar( $current ) ? (string) $current : wp_json_encode( $current );


			if ( (string) $current === (string) $normalized ) {
				return false;
			}

			update_post_meta( $order_id, $meta_key, $normalized );
			return true;

		} catch ( Exception $e ) {
			error_log(
				'RouteApp: error updating order meta (' . $meta_key . '): ' . $e->getMessage()
			);
			return false;
		}
	}

	//MSS-6533
	/**
	 * Resolve the local pickup flag in a FAIL-SAFE way.
	 *
	 * Priority:
	 * 1. Order shipping items
	 * 3. Checkout session (fallback only)
	 *
	 * @param WC_Order|null $order
	 *
	 * @return int 1 = local pickup, 0 = not local pickup
	 */
	public function routeapp_resolve_ship_to_store_flag( WC_Order $order = null): int {
		$excluded_methods = $this->routeapp_get_excluded_shipping_methods();
		if ( empty( $excluded_methods ) || ! is_array( $excluded_methods ) ) {
			return 0;
		}

		// Order-based (most reliable).
		if ( $order instanceof WC_Order ) {
			$shipping_items = $order->get_shipping_methods();
			if ( ! empty( $shipping_items ) ) {
				foreach ( $shipping_items as $shipping_item ) {
					if ( ! $shipping_item instanceof WC_Order_Item_Shipping ) {
						continue;
					}
					$method_id = $shipping_item->get_method_id();
					if ( empty( $method_id ) ) {
						continue;
					}
					$method_id = explode( ':', $method_id )[0];
					if ( $this->routeapp_is_shipping_method_excluded( $method_id ) ) {
						return 1;
					}
				}
				return 0;
			}
		}

		// Session fallback (checkout only).
		if ( ! function_exists( 'WC' ) || ! WC()->session ) {
			return 0;
		}
		$chosen = WC()->session->get( 'chosen_shipping_methods' );
		$packages = WC()->session->get( 'shipping_for_package_0' );
		if ( empty( $chosen[0] ) || empty( $packages['rates'] ) ) {
			return 0;
		}
		$chosen_id = $chosen[0];
		if ( ! isset( $packages['rates'][ $chosen_id ] ) ) {
			return 0;
		}
		$method_id = explode( ':', $packages['rates'][ $chosen_id ]->id )[0];
		if ( $this->routeapp_is_shipping_method_excluded( $method_id ) ) {
			return 1;
		}

		return 0;
	}

	/**
	 * Register callback for WooCommerce blocks checkout cart updates
	 * This ensures the Route widget toggle properly updates the cart in blocks checkout
	 *
	 * @return void
	 */
	public function routeapp_register_blocks_checkout_callback() {
		if ( function_exists( 'woocommerce_store_api_register_update_callback' ) ) {
			woocommerce_store_api_register_update_callback(
				array(
					'namespace' => 'route-widget-integration',
					'callback'  => function( $data ) {
						// Update session if checkbox value is provided
						if ( isset( $data['checkbox'] ) && WC()->session ) {
                            $checkboxValue = filter_var( $data['checkbox'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );

                            // Treat invalid values as false to keep session state deterministic.
                            WC()->session->set( 'checkbox_checked', true === $checkboxValue );
						}

						// The cart will be recalculated automatically, triggering woocommerce_cart_calculate_fees
						return true;
					},
				)
			);
		}
	}

	/**
	 * Inject Route widget before the checkout block order summary (default placement for blocks checkout).
	 *
	 * @param string $block_content The block content.
	 * @param array  $block         The full block, including name and attributes.
	 * @param object $instance      The block instance.
	 * @return string Modified block content with Route widget prepended when applicable.
	 */
	public function routeapp_checkout_block_before_order_summary( $blockContent, $block, $instance ) {
		if ( is_admin() && ! wp_doing_ajax() ) {
			return $blockContent;
		}

		if ( $this->route_block_rendered || $this->is_route_block_in_checkout_page() ) {
			return $blockContent;
		}

		if ( ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->session ) {
			return $blockContent;
		}
		if ( empty( $this->routeapp_get_public_token() ) ) {
			return $blockContent;
		}

		WC()->cart->calculate_totals();

		$routeHtml = do_shortcode( '[route]' );
		if ( empty( $routeHtml ) ) {
			return $blockContent;
		}

		return $routeHtml . $blockContent;
	}

	/**
	 * Register the Route Protection custom checkout block.
	 * Hooked to 'init'.
	 */
	public function routeapp_register_checkout_block() {
		if ( ! function_exists( 'register_block_type' ) ) {
			return;
		}

		$block_dir = plugin_dir_path( dirname( __FILE__ ) ) . 'blocks/route-protection';
		if ( ! file_exists( $block_dir . '/block.json' ) ) {
			return;
		}

		wp_register_script(
			'route-protection-editor',
			plugins_url( 'blocks/route-protection/index.js', dirname( __FILE__ ) ),
			array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wc-blocks-checkout' ),
			defined( 'ROUTEAPP_VERSION' ) ? ROUTEAPP_VERSION : '1.0.0',
			true
		);

		register_block_type(
			$block_dir,
			array(
				'editor_script'   => 'route-protection-editor',
				'render_callback' => array( $this, 'render_route_protection_block' ),
			)
		);
	}

	/**
	 * Server-side render callback for the route/route-protection block.
	 *
	 * @param array  $attributes Block attributes.
	 * @param string $content    Block inner content.
	 * @return string
	 */
	public function render_route_protection_block( $attributes, $content ) {
		if ( is_admin() && ! wp_doing_ajax() ) {
			return '';
		}
		if ( ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->session ) {
			return '';
		}
		if ( empty( $this->routeapp_get_public_token() ) ) {
			return '';
		}

		WC()->cart->calculate_totals();

		$routeHtml = do_shortcode( '[route]' );

		$this->route_block_rendered = true;

		return $routeHtml;
	}

	/**
	 * Register the Route Protection IntegrationInterface with WooCommerce Blocks.
	 *
	 * @param \Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry $integration_registry
	 */
	public function routeapp_register_blocks_integration( $integration_registry ) {
		$file = plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-routeapp-blocks-integration.php';
		if ( ! class_exists( 'Routeapp_Blocks_Integration' ) && file_exists( $file ) ) {
			require_once $file;
		}
		if ( class_exists( 'Routeapp_Blocks_Integration' ) ) {
			$integration_registry->register( new Routeapp_Blocks_Integration() );
		}
	}

	/**
	 * Whether the store checkout page uses the WooCommerce Checkout block.
	 *
	 * @return bool
	 */
	private function routeapp_is_wc_blocks_checkout_page() {
		if ( class_exists( '\Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils' ) ) {
			return \Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils::is_checkout_block_default();
		}

		if ( ! function_exists( 'wc_get_page_id' ) || ! function_exists( 'has_block' ) ) {
			return false;
		}

		$checkout_page_id = wc_get_page_id( 'checkout' );
		return $checkout_page_id > 0 && has_block( 'woocommerce/checkout', $checkout_page_id );
	}

	/**
	 * Whether routeapp-public-pbc.js should depend on wc-blocks-checkout.
	 *
	 * Classic checkout stores often deregister block scripts after they are registered.
	 * Adding wc-blocks-checkout as a dependency in that case causes WordPress to silently
	 * drop routeapp-public-pbc.js when the handle is later deregistered.
	 *
	 * @return bool
	 */
	private function routeapp_should_add_wc_blocks_checkout_dependency() {
		if ( ! wp_script_is( 'wc-blocks-checkout', 'registered' ) ) {
			return false;
		}

		if ( doing_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' ) ) {
			return true;
		}

		if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
			return false;
		}

		return $this->routeapp_is_wc_blocks_checkout_page();
	}

	/**
	 * Check whether the checkout page contains the route/route-protection block.
	 *
	 * @return bool
	 */
	private function is_route_block_in_checkout_page() {
		if ( ! function_exists( 'wc_get_page_id' ) || ! function_exists( 'has_block' ) ) {
			return false;
		}

		$checkout_page_id = wc_get_page_id( 'checkout' );
		if ( $checkout_page_id <= 0 ) {
			return false;
		}

		$post = get_post( $checkout_page_id );
		if ( ! $post ) {
			return false;
		}

		return has_block( 'route/route-protection', $post );
	}

}


```
