Plugin Directory

Changeset 3647198


Ignore:
Timestamp:
08/14/2026 11:07:46 AM (2 months ago)
Author:
exosi
Message:

add: new version added

Location:
express-one-shipment
Files:
51 added
4 edited

Legend:

Unmodified
Added
Removed
  • express-one-shipment/trunk/assets/js/checkout-map.js

    r3494529 r3647198  
    113113  function isPickupShippingMethod(methodValue) {
    114114    var cfg = getCfg();
    115     if (!cfg) return false;
    116     var methodId = cfg.shippingMethodId || 'express_one_pickup_shipping';
    117     return methodValue && methodValue.indexOf(methodId) !== -1;
     115    if (!cfg || !methodValue) return false;
     116    var methodIds = cfg.shippingMethodIds || [cfg.shippingMethodId || 'express_one_pickup_shipping'];
     117    for (var i = 0; i < methodIds.length; i++) {
     118      if (methodValue.indexOf(methodIds[i]) !== -1) return true;
     119    }
     120    return false;
    118121  }
    119122
  • express-one-shipment/trunk/changelog.txt

    r3636558 r3647198  
    43432026-08-06 - version 1.0.7
    4444* Fix - "cargo" flag set to false when creating a new shipment (express_one_handle_create_shipment); the shipment update request (express_one_handle_update_shipment) still sends "cargo" as true
     45
     462026-08-14 - version 1.1.0
     47* Add - New "Pickup Shipment (Weight-Based)" and "Home Delivery Shipment (Weight-Based)" shipping methods, priced dynamically by parcel weight, grouped per destination country with a "Default" fallback group for any country without an explicit override
     48* Add - Optional maximum parcel size (sum of length + width + height) limit for the weight-based Pickup method
     49* Add - The new methods reuse the existing pickup-point map selection and Home Delivery courier API eligibility checks
     50* Add - Weight-based methods become unavailable at checkout if the cart's destination country/weight falls outside every configured tier, or if any cart item is missing its Weight (or, for Pickup with a size limit set, its Length/Width/Height) — unless the cart already qualifies for free shipping
     51* Add - Weight-based methods and their original (fixed-rate) counterparts are now mutually exclusive: enabling one automatically disables the other, with an admin notice explaining why
     52* Add - New admin settings pages under Express One for the weight-based methods
     53* Dev - Internal refactor, no functional changes to the existing fixed-rate methods: split the single plugin file into includes/ (shipping-helpers, pickup-map, order-integration, admin-settings, one file per shipping method class) and includes/traits/, deduplicating postcode matching, country matching, shipping-method registration, and the negative-price cart label that were previously copy-pasted across the Pickup and Home Delivery classes
     54* Dev - Verified byte-for-byte: all API endpoint URLs, nonce action strings, existing saved option names, and the two original shipping method IDs are unchanged, so existing shipping zone configuration and saved settings keep working without any action needed
  • express-one-shipment/trunk/express-one-shipment.php

    r3636558 r3647198  
    55Text Domain: express-one-shipment
    66Domain Path: /languages
    7 Version: 1.0.7
     7Version: 1.1.0
    88Author: Express One
    99Author URI: https://expressone.si/
    … …  
    1616
    1717if ( ! defined( 'EXPRESS_ONE_SHIPMENT_VERSION' ) ) {
    18         define( 'EXPRESS_ONE_SHIPMENT_VERSION', '1.0.7' );
     18        define( 'EXPRESS_ONE_SHIPMENT_VERSION', '1.1.0' );
    1919}
    2020
    … …  
    2727}
    2828
    29 /**
    30  * Initialize WP_Filesystem and return it.
    31  *
    32  * @return WP_Filesystem_Base|false
    33  */
    34 function express_one_get_filesystem() {
    35         global $wp_filesystem;
     29// Shared helpers, then the traits the four shipping method classes use.
     30require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/shipping-helpers.php';
     31require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/traits/trait-shipping-common-fields.php';
     32require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/traits/trait-country-weight-tiers-field.php';
    3633
    37         if ( $wp_filesystem instanceof WP_Filesystem_Base ) {
    38                 return $wp_filesystem;
    39         }
     34// Pickup-point map: parcel shop API fetch, Leaflet asset loading, checkout
     35// validation (classic + Store API), AJAX endpoints.
     36require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/pickup-map.php';
    4037
    41         require_once ABSPATH . 'wp-admin/includes/file.php';
    42         $initialized = WP_Filesystem();
     38// The four WooCommerce shipping method classes.
     39require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/class-express-one-pickup-shipping.php';
     40require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/class-express-one-flat-shipping.php';
     41require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/class-express-one-pickup-weight-shipping.php';
     42require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/class-express-one-flat-weight-shipping.php';
    4343
    44         if ( ! $initialized || ! ( $wp_filesystem instanceof WP_Filesystem_Base ) ) {
    45                 return false;
    46         }
     44// Order meta display, admin order buttons (incl. collie count), label
     45// printing (single + bulk), and the Express One shipment create/update calls.
     46require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/order-integration.php';
    4747
    48         return $wp_filesystem;
    49 }
    50 
    51 /**
    52  * Check whether WooCommerce HPOS (custom orders table) is enabled.
    53  *
    54  * @return bool
    55  */
    56 function express_one_is_hpos_enabled() {
    57         return class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' )
    58                 && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
    59 }
    60 
    61 /**
    62  * Get the WP-admin edit URL for an order (HPOS-compatible).
    63  *
    64  * @param int $order_id
    65  * @return string
    66  */
    67 function express_one_get_order_edit_url( $order_id ) {
    68         if ( express_one_is_hpos_enabled() ) {
    69                 return admin_url( 'admin.php?page=wc-orders&action=edit&id=' . (int) $order_id );
    70         }
    71         return admin_url( 'post.php?post=' . (int) $order_id . '&action=edit' );
    72 }
    73 
    74 /**
    75  * Verify WooCommerce checkout nonce for hooks that read $_POST.
    76  *
    77  * @return bool
    78  */
    79 function express_one_verify_wc_checkout_nonce() {
    80         $nonce = isset( $_POST['woocommerce-process-checkout-nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['woocommerce-process-checkout-nonce'] ) ) : '';
    81         if ( empty( $nonce ) ) {
    82                 return false;
    83         }
    84         return (bool) wp_verify_nonce( $nonce, 'woocommerce-process_checkout' );
    85 }
    86 
    87 /**
    88  * Frontend assets (checkout)
    89  */
    90 function express_one_enqueue_frontend_assets() {
    91         if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
    92                 return;
    93         }
    94 
    95         wp_enqueue_style(
    96                 'express-one-leaflet',
    97                 EXPRESS_ONE_SHIPMENT_URL . 'assets/vendor/leaflet/leaflet.css',
    98                 [],
    99                 EXPRESS_ONE_SHIPMENT_VERSION
    100         );
    101 
    102         wp_enqueue_style(
    103                 'express-one-leaflet-geocoder',
    104                 EXPRESS_ONE_SHIPMENT_URL . 'assets/vendor/leaflet/leaflet-control-geocoder.css',
    105                 [ 'express-one-leaflet' ],
    106                 EXPRESS_ONE_SHIPMENT_VERSION
    107         );
    108 
    109         wp_enqueue_script(
    110                 'express-one-leaflet',
    111                 EXPRESS_ONE_SHIPMENT_URL . 'assets/vendor/leaflet/leaflet.js',
    112                 [],
    113                 EXPRESS_ONE_SHIPMENT_VERSION,
    114                 true
    115         );
    116 
    117         wp_enqueue_script(
    118                 'express-one-leaflet-geocoder',
    119                 EXPRESS_ONE_SHIPMENT_URL . 'assets/vendor/leaflet/leaflet-control-geocoder.js',
    120                 [ 'express-one-leaflet' ],
    121                 EXPRESS_ONE_SHIPMENT_VERSION,
    122                 true
    123         );
    124 
    125         wp_enqueue_script(
    126                 'express-one-checkout-map',
    127                 EXPRESS_ONE_SHIPMENT_URL . 'assets/js/checkout-map.js',
    128                 [ 'jquery', 'express-one-leaflet', 'express-one-leaflet-geocoder' ],
    129                 EXPRESS_ONE_SHIPMENT_VERSION,
    130                 true
    131         );
    132 
    133         $api_key = express_one_get_api_key();
    134         $points  = [];
    135 
    136         if ( ! empty( $api_key ) ) {
    137                 $api_url  = 'https://api.expressone.si/parcelshops?isActive=true&apikey=' . rawurlencode( $api_key );
    138                 $response = wp_remote_get(
    139                         $api_url,
    140                         [
    141                                 'timeout'   => 15,
    142                                 'sslverify' => true,
    143                         ]
    144                 );
    145 
    146                 if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
    147                         $data = json_decode( wp_remote_retrieve_body( $response ), true );
    148 
    149                         if ( isset( $data['data'] ) && is_array( $data['data'] ) ) {
    150                                 foreach ( $data['data'] as $point ) {
    151                                         if (
    152                                                 ! empty( $point['GeoLat'] ) && ! empty( $point['GeoLong'] ) &&
    153                                                 0 != $point['GeoLat'] && 0 != $point['GeoLong'] &&
    154                                                 ! empty( $point['IsActive'] ) && true === $point['IsActive']
    155                                         ) {
    156                                                 $points[] = [
    157                                                         'lat'     => $point['GeoLat'],
    158                                                         'lng'     => $point['GeoLong'],
    159                                                         'name'    => ( $point['ShortName'] ?? '' ) . ' - ' . ( $point['Address']['Place'] ?? '' ),
    160                                                         'street'  => $point['Address']['Street'] ?? '',
    161                                                         'zip'     => $point['Address']['ZipCode'] ?? '',
    162                                                         'city'    => $point['Address']['Place'] ?? '',
    163                                                         'OOHType' => $point['OOHType'] ?? 0,
    164                                                 ];
    165                                         }
    166                                 }
    167                         }
    168                 }
    169         }
    170 
    171         $blue_icon_url   = EXPRESS_ONE_SHIPMENT_URL . 'assets/images/marker-icon-blue.png';
    172         $red_icon_url    = EXPRESS_ONE_SHIPMENT_URL . 'assets/images/marker-icon-red.png';
    173         $shadow_icon_url = EXPRESS_ONE_SHIPMENT_URL . 'assets/images/marker-shadow.png';
    174 
    175         $localized_data = [
    176                 'points'           => $points,
    177                 'icons'            => [
    178                         'blue'   => $blue_icon_url,
    179                         'red'    => $red_icon_url,
    180                         'shadow' => $shadow_icon_url,
    181                 ],
    182                 'placeholder'      => __( 'Search for a location...', 'express-one-shipment' ),
    183                 'shippingMethodId' => 'express_one_pickup_shipping',
    184                 'mapContainerId'   => 'express-one-map',
    185                 'ajaxUrl'          => admin_url( 'admin-ajax.php' ),
    186                 'nonce'            => wp_create_nonce( 'express_one_pickup_nonce' ),
    187         ];
    188 
    189         wp_localize_script(
    190                 'express-one-checkout-map',
    191                 'expressOnePickup',
    192                 $localized_data
    193         );
    194 }
    195 add_action( 'wp_enqueue_scripts', 'express_one_enqueue_frontend_assets' );
    196 
    197 /**
    198  * Admin assets (inline CSS for orders list button)
    199  */
    200 function express_one_admin_enqueue_assets( $hook ) {
    201         if ( 'woocommerce_page_wc-orders' !== $hook ) {
    202                 return;
    203         }
    204 
    205         wp_register_style(
    206                 'express-one-admin-inline',
    207                 false,
    208                 [],
    209                 EXPRESS_ONE_SHIPMENT_VERSION,
    210                 'all'
    211         );
    212         wp_enqueue_style( 'express-one-admin-inline' );
    213 
    214         $slug = 'download_label';
    215         $css  = '
    216                 .wc-action-button-' . $slug . '::after {
    217                         font-family: inherit !important;
    218                         content: "\2B07" !important;
    219                         font-size: 14px;
    220                         display: inline-block;
    221                         line-height: 1;
    222                 }
    223                 .wp-admin .woocommerce-page .widefat .wc-action-button-' . $slug . ':hover {
    224                         background-color: #7ad03a !important;
    225                         color: #fff !important;
    226                 }
    227                 .wc-action-button-' . $slug . ' {
    228                         cursor: pointer;
    229                         text-decoration: none;
    230                 }
    231         ';
    232 
    233         wp_add_inline_style( 'express-one-admin-inline', $css );
    234 }
    235 add_action( 'admin_enqueue_scripts', 'express_one_admin_enqueue_assets' );
    236 
    237 /**
    238  * Admin JS for main settings page
    239  */
    240 function express_one_admin_enqueue_main_settings_script( $hook ) {
    241         if ( 'toplevel_page_express-one-settings' !== $hook ) {
    242                 return;
    243         }
    244 
    245         wp_enqueue_script(
    246                 'express-one-admin-main-settings',
    247                 EXPRESS_ONE_SHIPMENT_URL . 'assets/js/admin-main-settings.js',
    248                 [ 'jquery' ],
    249                 EXPRESS_ONE_SHIPMENT_VERSION,
    250                 true
    251         );
    252 
    253         $data = [
    254                 'ajaxUrl'            => admin_url( 'admin-ajax.php' ),
    255                 'nonce'              => wp_create_nonce( 'express_one_ajax_nonce' ),
    256                 'loadingLabel'       => __( 'Loading...', 'express-one-shipment' ),
    257                 'selectLabelText'    => __( 'Select a label', 'express-one-shipment' ),
    258                 'errorFetchText'     => __( 'Error fetching layouts', 'express-one-shipment' ),
    259                 'initialLabelLayout' => express_one_get_label_layout_id(),
    260         ];
    261 
    262         wp_localize_script(
    263                 'express-one-admin-main-settings',
    264                 'expressOneAdmin',
    265                 $data
    266         );
    267 }
    268 add_action( 'admin_enqueue_scripts', 'express_one_admin_enqueue_main_settings_script' );
    269 
    270 /**
    271  * Admin JS for collie count on order edit screen
    272  */
    273 function express_one_admin_enqueue_collie_count_script( $hook ) {
    274         // Legacy order edit screen.
    275         $is_legacy = ( 'post.php' === $hook );
    276         if ( $is_legacy ) {
    277                 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
    278                 if ( ! $screen || 'shop_order' !== $screen->post_type ) {
    279                         $is_legacy = false;
    280                 }
    281         }
    282 
    283         // HPOS order edit screen.
    284         $is_hpos = ( 'woocommerce_page_wc-orders' === $hook );
    285 
    286         if ( ! $is_legacy && ! $is_hpos ) {
    287                 return;
    288         }
    289 
    290         wp_enqueue_script(
    291                 'express-one-admin-collie-count',
    292                 EXPRESS_ONE_SHIPMENT_URL . 'assets/js/admin-collie-count.js',
    293                 [],
    294                 EXPRESS_ONE_SHIPMENT_VERSION,
    295                 true
    296         );
    297 
    298         wp_localize_script(
    299                 'express-one-admin-collie-count',
    300                 'expressOneCollie',
    301                 [
    302                         'modalTitle'   => __( 'How many collies?', 'express-one-shipment' ),
    303                         'modalDesc'    => __( 'Enter the number of collies for this shipment (max %d).', 'express-one-shipment' ),
    304                         'confirmLabel' => __( 'Confirm', 'express-one-shipment' ),
    305                         'cancelLabel'  => __( 'Cancel', 'express-one-shipment' ),
    306                 ]
    307         );
    308 }
    309 add_action( 'admin_enqueue_scripts', 'express_one_admin_enqueue_collie_count_script' );
    310 
    311 /**
    312  * AJAX: set pickup (checkout)
    313  */
    314 add_action( 'wp_ajax_express_one_set_pickup', 'express_one_set_pickup' );
    315 add_action( 'wp_ajax_nopriv_express_one_set_pickup', 'express_one_set_pickup' );
    316 function express_one_set_pickup() {
    317         $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_key( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
    318         if ( 'post' !== strtolower( $request_method ) ) {
    319                 wp_send_json_error( 'invalid_method' );
    320         }
    321 
    322         $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
    323         if ( ! wp_verify_nonce( $nonce, 'express_one_pickup_nonce' ) ) {
    324                 wp_send_json_error( 'invalid_nonce' );
    325         }
    326 
    327         if ( ! function_exists( 'WC' ) || ! WC()->session ) {
    328                 wp_send_json_error( 'no_session' );
    329         }
    330 
    331         $point   = sanitize_text_field( wp_unslash( $_POST['point'] ?? '' ) );
    332         $addr    = sanitize_text_field( wp_unslash( $_POST['address'] ?? '' ) );
    333         $zip     = sanitize_text_field( wp_unslash( $_POST['postcode'] ?? '' ) );
    334         $city    = sanitize_text_field( wp_unslash( $_POST['city'] ?? '' ) );
    335         $country = sanitize_text_field( wp_unslash( $_POST['country'] ?? 'SI' ) );
    336 
    337         if ( '' === $point || '' === $addr || '' === $zip || '' === $city ) {
    338                 wp_send_json_error( 'missing_fields' );
    339         }
    340 
    341         if ( ! preg_match( '/^[A-Z]{2}$/', strtoupper( $country ) ) ) {
    342                 $country = 'SI';
    343         } else {
    344                 $country = strtoupper( $country );
    345         }
    346 
    347         WC()->session->set( 'express_one_pickup_point', $point );
    348         WC()->session->set( 'express_one_pickup_address', $addr );
    349         WC()->session->set( 'express_one_pickup_postcode', $zip );
    350         WC()->session->set( 'express_one_pickup_city', $city );
    351         WC()->session->set( 'express_one_pickup_country', $country );
    352 
    353         wp_send_json_success();
    354 }
    355 
    356 add_action(
    357         'woocommerce_before_calculate_totals',
    358         function () {
    359                 if ( ! function_exists( 'WC' ) || ! WC()->session || ! WC()->customer ) {
    360                         return;
    361                 }
    362 
    363                 $chosen = WC()->session->get( 'chosen_shipping_methods' );
    364                 $method = is_array( $chosen ) ? ( $chosen[0] ?? '' ) : '';
    365 
    366                 if ( $method !== 'express_one_pickup_shipping' ) {
    367                         return;
    368                 }
    369 
    370                 $addr    = WC()->session->get( 'express_one_pickup_address' );
    371                 $zip     = WC()->session->get( 'express_one_pickup_postcode' );
    372                 $city    = WC()->session->get( 'express_one_pickup_city' );
    373                 $country = WC()->session->get( 'express_one_pickup_country' );
    374 
    375                 if ( empty( $addr ) || empty( $zip ) || empty( $city ) ) {
    376                         return;
    377                 }
    378 
    379                 WC()->customer->set_shipping_address_1( $addr );
    380                 WC()->customer->set_shipping_postcode( $zip );
    381                 WC()->customer->set_shipping_city( $city );
    382                 WC()->customer->set_shipping_country( $country ?: 'SI' );
    383         },
    384         20
    385 );
    386 
    387 /**
    388  * Save pickup point as shipping address (checkout)
    389  */
    390 add_action(
    391         'woocommerce_checkout_create_order',
    392         function ( $order ) {
    393 
    394                 // Verify WooCommerce checkout nonce (Plugin Check-friendly).
    395                 $nonce = isset( $_POST['woocommerce-process-checkout-nonce'] )
    396                         ? sanitize_text_field( wp_unslash( $_POST['woocommerce-process-checkout-nonce'] ) )
    397                         : '';
    398 
    399                 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'woocommerce-process_checkout' ) ) {
    400                         return;
    401                 }
    402 
    403                 // Read POST only AFTER nonce verification.
    404                 $selected_point = isset( $_POST['express_one_selected_point'] )
    405                         ? sanitize_text_field( wp_unslash( $_POST['express_one_selected_point'] ) )
    406                         : '';
    407 
    408                 if ( '' === $selected_point ) {
    409                         return;
    410                 }
    411 
    412                 $address  = isset( $_POST['express_one_address'] ) ? sanitize_text_field( wp_unslash( $_POST['express_one_address'] ) ) : '';
    413                 $postcode = isset( $_POST['express_one_postcode'] ) ? sanitize_text_field( wp_unslash( $_POST['express_one_postcode'] ) ) : '';
    414                 $city     = isset( $_POST['express_one_city'] ) ? sanitize_text_field( wp_unslash( $_POST['express_one_city'] ) ) : '';
    415                 $country  = isset( $_POST['express_one_country'] ) ? sanitize_text_field( wp_unslash( $_POST['express_one_country'] ) ) : '';
    416 
    417                 $order->set_shipping_address_1( $address );
    418                 $order->set_shipping_postcode( $postcode );
    419                 $order->set_shipping_city( $city );
    420                 $order->set_shipping_country( $country );
    421                 $order->add_meta_data( 'Pickup Point', $selected_point, true );
    422         },
    423         10,
    424         1
    425 );
    426 
    427 /**
    428  * Render hidden fields inside the checkout form so JS can populate them on submit.
    429  * woocommerce_after_order_notes fires inside <form.checkout> but outside the
    430  * AJAX-replaced #order_review fragment, so values survive checkout updates.
    431  */
    432 add_action( 'woocommerce_after_order_notes', 'express_one_render_hidden_checkout_fields' );
    433 function express_one_render_hidden_checkout_fields() {
    434         echo '<input type="hidden" id="express_one_selected_point" name="express_one_selected_point" value="">';
    435         echo '<input type="hidden" id="oe_address" name="express_one_address" value="">';
    436         echo '<input type="hidden" id="oe_postcode" name="express_one_postcode" value="">';
    437         echo '<input type="hidden" id="oe_city" name="express_one_city" value="">';
    438         echo '<input type="hidden" id="oe_country" name="express_one_country" value="">';
    439 }
    440 
    441 /**
    442  * Block checkout if pickup point is required but not selected
    443  */
    444 add_action( 'woocommerce_after_checkout_validation', 'express_one_validate_shipping', 10, 2 );
    445 
    446 function express_one_validate_shipping( $data, $errors ) {
    447 
    448         // ✅ Explicit WooCommerce checkout nonce verification (Plugin Check-friendly).
    449         $nonce = isset( $_POST['woocommerce-process-checkout-nonce'] )
    450                 ? sanitize_text_field( wp_unslash( $_POST['woocommerce-process-checkout-nonce'] ) )
    451                 : '';
    452 
    453         if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'woocommerce-process_checkout' ) ) {
    454                 $errors->add(
    455                         'expressone-nonce',
    456                         esc_html__( 'Security check failed. Please refresh the page and try again.', 'express-one-shipment' )
    457                 );
    458                 return;
    459         }
    460 
    461         $chosen_methods = WC()->session->get( 'chosen_shipping_methods' );
    462         if ( empty( $chosen_methods ) || ! is_array( $chosen_methods ) ) {
    463                 return;
    464         }
    465 
    466         $selected_method = $chosen_methods[0];
    467 
    468         // Read POST only AFTER nonce verification.
    469         $selected_point = isset( $_POST['express_one_selected_point'] )
    470                 ? sanitize_text_field( wp_unslash( $_POST['express_one_selected_point'] ) )
    471                 : '';
    472 
    473         if ( 'express_one_pickup_shipping' === $selected_method ) {
    474                 if ( '' === $selected_point ) {
    475                         $errors->add(
    476                                 'no-pickup',
    477                                 esc_html__( 'You must select a pickup point before completing your order.', 'express-one-shipment' )
    478                         );
    479                 }
    480         }
    481 
    482         if ( 'express_one_flat_shipping' === $selected_method ) {
    483                 $country = isset( $data['shipping_country'] ) ? strtoupper( sanitize_text_field( $data['shipping_country'] ) ) : 'SI';
    484 
    485                 if ( 'SI' !== $country ) {
    486                         $api_key  = express_one_get_api_key();
    487                         $response = wp_remote_post(
    488                                 add_query_arg( [ 'apikey' => $api_key ], 'https://api.expressone.si/checkcountryiseligible' ),
    489                                 [
    490                                         'timeout' => 10,
    491                                         'headers' => [ 'Content-Type' => 'application/json' ],
    492                                         'body'    => wp_json_encode( [ 'countryCode' => $country ] ),
    493                                 ]
    494                         );
    495 
    496                         if ( is_wp_error( $response ) ) {
    497                                 $errors->add(
    498                                         'api-fail',
    499                                         esc_html__( 'Unable to validate shipping availability. Please try again.', 'express-one-shipment' )
    500                                 );
    501                                 return;
    502                         }
    503 
    504                         $body = json_decode( wp_remote_retrieve_body( $response ), true );
    505 
    506                         if ( empty( $body['data']['isEligible'] ) ) {
    507                                 $errors->add(
    508                                         'country-not-eligible',
    509                                         esc_html__( 'The provider does not support shipping to this country.', 'express-one-shipment' )
    510                                 );
    511                                 return;
    512                         }
    513 
    514                         return;
    515                 }
    516 
    517                 $city    = isset( $data['shipping_city'] ) ? sanitize_text_field( $data['shipping_city'] ) : '';
    518                 $zipcode = isset( $data['shipping_postcode'] ) ? sanitize_text_field( $data['shipping_postcode'] ) : '';
    519 
    520                 if ( '' === $city || '' === $zipcode ) {
    521                         return;
    522                 }
    523 
    524                 $api_key  = express_one_get_api_key();
    525                 $endpoint = 'https://api.expressone.si/places';
    526 
    527                 $url = add_query_arg(
    528                         [
    529                                 'name'    => $city,
    530                                 'zipcode' => $zipcode,
    531                                 'approx'  => 'true',
    532                                 'apikey'  => $api_key,
    533                         ],
    534                         $endpoint
    535                 );
    536 
    537                 $response = wp_remote_get(
    538                         $url,
    539                         [
    540                                 'timeout' => 10,
    541                         ]
    542                 );
    543 
    544                 if ( is_wp_error( $response ) ) {
    545                         $errors->add(
    546                                 'api-fail',
    547                                 esc_html__( 'Unable to validate cargo availability. Please try again.', 'express-one-shipment' )
    548                         );
    549                         return;
    550                 }
    551 
    552                 $body = json_decode( wp_remote_retrieve_body( $response ), true );
    553 
    554                 if ( empty( $body['data'] ) ) {
    555                         $errors->add(
    556                                 'no-service',
    557                                 esc_html__( 'The cargo company does not ship to your address.', 'express-one-shipment' )
    558                         );
    559                         return;
    560                 }
    561 
    562                 $row = $body['data'][0];
    563 
    564                 if ( empty( $row['StandardAvailable'] ) || empty( $row['CargoAvailable'] ) ) {
    565                         $errors->add(
    566                                 'not-available',
    567                                 esc_html__( 'This shipment method does not provide cargo service to this address.', 'express-one-shipment' )
    568                         );
    569                         return;
    570                 }
    571         }
    572 }
    573 
    574 /**
    575  * Blocks checkout validation (WooCommerce Store API).
    576  */
    577 add_action(
    578         'woocommerce_store_api_checkout_update_order_from_request',
    579         function ( WC_Order $order, WP_REST_Request $request ) {
    580 
    581                 $chosen_methods  = WC()->session ? WC()->session->get( 'chosen_shipping_methods' ) : [];
    582                 $selected_method = is_array( $chosen_methods ) && ! empty( $chosen_methods ) ? $chosen_methods[0] : '';
    583 
    584                 if ( 'express_one_flat_shipping' !== $selected_method ) {
    585                         return;
    586                 }
    587 
    588                 $country = strtoupper( $order->get_shipping_country() ?: $order->get_billing_country() ?: 'SI' );
    589                 $api_key = express_one_get_api_key();
    590 
    591                 if ( 'SI' !== $country ) {
    592                         $response = wp_remote_post(
    593                                 add_query_arg( [ 'apikey' => $api_key ], 'https://api.expressone.si/checkcountryiseligible' ),
    594                                 [
    595                                         'timeout' => 10,
    596                                         'headers' => [ 'Content-Type' => 'application/json' ],
    597                                         'body'    => wp_json_encode( [ 'countryCode' => $country ] ),
    598                                 ]
    599                         );
    600 
    601                         if ( is_wp_error( $response ) ) {
    602                                 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
    603                                         'express-one-api-fail',
    604                                         esc_html__( 'Unable to validate shipping availability. Please try again.', 'express-one-shipment' ),
    605                                         400
    606                                 );
    607                         }
    608 
    609                         $body = json_decode( wp_remote_retrieve_body( $response ), true );
    610 
    611                         if ( empty( $body['data']['isEligible'] ) ) {
    612                                 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
    613                                         'express-one-country-not-eligible',
    614                                         esc_html__( 'The provider does not support shipping to this country.', 'express-one-shipment' ),
    615                                         400
    616                                 );
    617                         }
    618 
    619                         return;
    620                 }
    621 
    622                 $city    = $order->get_shipping_city() ?: $order->get_billing_city();
    623                 $zipcode = $order->get_shipping_postcode() ?: $order->get_billing_postcode();
    624 
    625                 if ( empty( $city ) || empty( $zipcode ) ) {
    626                         return;
    627                 }
    628 
    629                 $response = wp_remote_get(
    630                         add_query_arg(
    631                                 [
    632                                         'name'    => $city,
    633                                         'zipcode' => $zipcode,
    634                                         'approx'  => 'true',
    635                                         'apikey'  => $api_key,
    636                                 ],
    637                                 'https://api.expressone.si/places'
    638                         ),
    639                         [ 'timeout' => 10 ]
    640                 );
    641 
    642                 if ( is_wp_error( $response ) ) {
    643                         throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
    644                                 'express-one-api-fail',
    645                                 esc_html__( 'Unable to validate cargo availability. Please try again.', 'express-one-shipment' ),
    646                                 400
    647                         );
    648                 }
    649 
    650                 $body = json_decode( wp_remote_retrieve_body( $response ), true );
    651 
    652                 if ( empty( $body['data'] ) ) {
    653                         throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
    654                                 'express-one-no-service',
    655                                 esc_html__( 'The cargo company does not ship to your address.', 'express-one-shipment' ),
    656                                 400
    657                         );
    658                 }
    659 
    660                 $row = $body['data'][0];
    661 
    662                 if ( empty( $row['StandardAvailable'] ) || empty( $row['CargoAvailable'] ) ) {
    663                         throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException(
    664                                 'express-one-not-available',
    665                                 esc_html__( 'This shipment method does not provide cargo service to this address.', 'express-one-shipment' ),
    666                                 400
    667                         );
    668                 }
    669         },
    670         10,
    671         2
    672 );
    673 
    674 // Add admin menu
    675 add_action( 'admin_menu', 'express_one_add_admin_menu' );
    676 
    677 function express_one_add_admin_menu() {
    678         add_menu_page(
    679                 __( 'Express One', 'express-one-shipment' ),
    680                 __( 'Express One', 'express-one-shipment' ),
    681                 'manage_woocommerce',
    682                 'express-one-settings',
    683                 'express_one_main_settings_page',
    684                 'dashicons-location-alt',
    685                 56
    686         );
    687 
    688         add_submenu_page(
    689                 'express-one-settings',
    690                 __( 'Pickup Shipment', 'express-one-shipment' ),
    691                 __( 'Pickup Shipment', 'express-one-shipment' ),
    692                 'manage_woocommerce',
    693                 'express-one-pickup',
    694                 'express_one_pickup_settings_page'
    695         );
    696 
    697         add_submenu_page(
    698                 'express-one-settings',
    699                 __( 'Home Delivery Shipment', 'express-one-shipment' ),
    700                 __( 'Home Delivery Shipment', 'express-one-shipment' ),
    701                 'manage_woocommerce',
    702                 'express-one-flat',
    703                 'express_one_flat_settings_page'
    704         );
    705 }
    706 
    707 function express_one_main_settings_page() {
    708         if ( isset( $_POST['express_one_save_main_settings'] ) ) {
    709                 check_admin_referer( 'express_one_main_settings' );
    710 
    711                 $api_key = sanitize_text_field( wp_unslash( $_POST['api_key'] ?? '' ) );
    712 
    713                 $validation_response = wp_remote_get(
    714                         'https://api.expressone.si/apiuserinfo?apikey=' . rawurlencode( $api_key ),
    715                         [
    716                                 'timeout'   => 15,
    717                                 'sslverify' => true,
    718                         ]
    719                 );
    720 
    721                 if ( is_wp_error( $validation_response ) ) {
    722                         $error_message = $validation_response->get_error_message();
    723                         echo '<div class="notice notice-error is-dismissible"><p>' .
    724                                 sprintf(
    725                     /* translators: %s: error message returned by the API request */
    726                                         esc_html__( 'API Key validation failed: %s', 'express-one-shipment' ),
    727                                         esc_html( $error_message )
    728                                 ) .
    729                                 '</p></div>';
    730                 } else {
    731                         $status_code = wp_remote_retrieve_response_code( $validation_response );
    732                         if ( 200 === $status_code ) {
    733                                 update_option( 'express_one_api_key', $api_key );
    734                                 update_option( 'express_one_sender_name', sanitize_text_field( wp_unslash( $_POST['sender_name'] ?? '' ) ) );
    735                                 update_option( 'express_one_sender_country', sanitize_text_field( wp_unslash( $_POST['sender_country'] ?? '' ) ) );
    736                                 update_option( 'express_one_sender_zipcode', sanitize_text_field( wp_unslash( $_POST['sender_zipcode'] ?? '' ) ) );
    737                                 update_option( 'express_one_sender_city', sanitize_text_field( wp_unslash( $_POST['sender_city'] ?? '' ) ) );
    738                                 update_option( 'express_one_sender_street', sanitize_text_field( wp_unslash( $_POST['sender_street'] ?? '' ) ) );
    739                                 update_option( 'express_one_sender_telephone', sanitize_text_field( wp_unslash( $_POST['sender_telephone'] ?? '' ) ) );
    740                                 update_option( 'express_one_sender_fax', sanitize_text_field( wp_unslash( $_POST['sender_fax'] ?? '' ) ) );
    741                                 update_option( 'express_one_sender_gsm', sanitize_text_field( wp_unslash( $_POST['sender_gsm'] ?? '' ) ) );
    742                                 update_option( 'express_one_sender_email', sanitize_email( wp_unslash( $_POST['sender_email'] ?? '' ) ) );
    743 
    744                                 if ( ! empty( $_POST['label_layout'] ) ) {
    745                                         update_option( 'express_one_label_layout_id', sanitize_text_field( wp_unslash( $_POST['label_layout'] ) ) );
    746                                 }
    747 
    748                                 echo '<div class="notice notice-success is-dismissible"><p>' .
    749                                         esc_html__( 'Settings saved successfully! API Key is valid.', 'express-one-shipment' ) .
    750                                         '</p></div>';
    751                         } else {
    752                                 echo '<div class="notice notice-error is-dismissible"><p>' .
    753                                         esc_html__( 'Invalid API Key!', 'express-one-shipment' ) .
    754                                         '</p></div>';
    755                         }
    756                 }
    757         }
    758 
    759         $api_key          = get_option( 'express_one_api_key', '' );
    760         $label_layout_id  = get_option( 'express_one_label_layout_id', '' );
    761         $sender_name      = get_option( 'express_one_sender_name', '' );
    762         $sender_country   = get_option( 'express_one_sender_country', 'SI' );
    763         $sender_zipcode   = get_option( 'express_one_sender_zipcode', '' );
    764         $sender_city      = get_option( 'express_one_sender_city', '' );
    765         $sender_street    = get_option( 'express_one_sender_street', '' );
    766         $sender_telephone = get_option( 'express_one_sender_telephone', '' );
    767         $sender_fax       = get_option( 'express_one_sender_fax', '' );
    768         $sender_gsm       = get_option( 'express_one_sender_gsm', '' );
    769         $sender_email     = get_option( 'express_one_sender_email', '' );
    770         ?>
    771         <div class="wrap">
    772                 <h1><?php esc_html_e( 'Express One - Main Settings', 'express-one-shipment' ); ?></h1>
    773                 <form method="post" action="">
    774                         <?php wp_nonce_field( 'express_one_main_settings' ); ?>
    775                         <table class="form-table">
    776                                 <tr>
    777                                         <th scope="row"><label for="api_key"><?php esc_html_e( 'API Key', 'express-one-shipment' ); ?></label></th>
    778                                         <td><input type="text" name="api_key" id="api_key" value="<?php echo esc_attr( $api_key ); ?>" class="regular-text" required /></td>
    779                                 </tr>
    780                                 <tr>
    781                                         <th scope="row"><label for="label_layout"><?php esc_html_e( 'Label Type Selection', 'express-one-shipment' ); ?></label></th>
    782                                         <td>
    783                                                 <select name="label_layout" id="label_layout" class="regular-text" disabled>
    784                                                         <option value=""><?php esc_html_e( 'Enter valid API to select label', 'express-one-shipment' ); ?></option>
    785                                                 </select>
    786                                                 <p class="description"><?php esc_html_e( 'Select label layout after entering a valid API key.', 'express-one-shipment' ); ?></p>
    787                                         </td>
    788                                 </tr>
    789                                 <tr><th colspan="2"><h2><?php esc_html_e( 'Sender Information', 'express-one-shipment' ); ?></h2></th></tr>
    790                                 <tr>
    791                                         <th scope="row"><label for="sender_name"><?php esc_html_e( 'Company Name', 'express-one-shipment' ); ?></label></th>
    792                                         <td><input type="text" name="sender_name" id="sender_name" value="<?php echo esc_attr( $sender_name ); ?>" class="regular-text" required /></td>
    793                                 </tr>
    794                                 <tr>
    795                                         <th scope="row"><label for="sender_country"><?php esc_html_e( 'Country Code', 'express-one-shipment' ); ?></label></th>
    796                                         <td>
    797                                                 <select name="sender_country" id="sender_country" class="regular-text" required>
    798                                                         <?php
    799                                                         if ( function_exists( 'WC' ) && WC()->countries ) {
    800                                                                 $countries = WC()->countries->get_countries();
    801                                                                 foreach ( $countries as $code => $name ) {
    802                                                                         printf(
    803                                                                                 '<option value="%1$s" %2$s>%3$s (%1$s)</option>',
    804                                                                                 esc_attr( $code ),
    805                                                                                 selected( $sender_country, $code, false ),
    806                                                                                 esc_html( $name )
    807                                                                         );
    808                                                                 }
    809                                                         }
    810                                                         ?>
    811                                                 </select>
    812                                         </td>
    813                                 </tr>
    814                                 <tr>
    815                                         <th scope="row"><label for="sender_zipcode"><?php esc_html_e( 'Zip Code', 'express-one-shipment' ); ?></label></th>
    816                                         <td><input type="text" name="sender_zipcode" id="sender_zipcode" value="<?php echo esc_attr( $sender_zipcode ); ?>" class="regular-text" required /></td>
    817                                 </tr>
    818                                 <tr>
    819                                         <th scope="row"><label for="sender_city"><?php esc_html_e( 'City', 'express-one-shipment' ); ?></label></th>
    820                                         <td><input type="text" name="sender_city" id="sender_city" value="<?php echo esc_attr( $sender_city ); ?>" class="regular-text" required /></td>
    821                                 </tr>
    822                                 <tr>
    823                                         <th scope="row"><label for="sender_street"><?php esc_html_e( 'Street and Number', 'express-one-shipment' ); ?></label></th>
    824                                         <td><input type="text" name="sender_street" id="sender_street" value="<?php echo esc_attr( $sender_street ); ?>" class="regular-text" required /></td>
    825                                 </tr>
    826                                 <tr>
    827                                         <th scope="row"><label for="sender_telephone"><?php esc_html_e( 'Telephone', 'express-one-shipment' ); ?></label></th>
    828                                         <td><input type="tel" name="sender_telephone" id="sender_telephone" value="<?php echo esc_attr( $sender_telephone ); ?>" class="regular-text" required /></td>
    829                                 </tr>
    830                                 <tr>
    831                                         <th scope="row"><label for="sender_fax"><?php esc_html_e( 'Fax', 'express-one-shipment' ); ?></label></th>
    832                                         <td><input type="tel" name="sender_fax" id="sender_fax" value="<?php echo esc_attr( $sender_fax ); ?>" class="regular-text" /></td>
    833                                 </tr>
    834                                 <tr>
    835                                         <th scope="row"><label for="sender_gsm"><?php esc_html_e( 'Notify GSM', 'express-one-shipment' ); ?></label></th>
    836                                         <td><input type="tel" name="sender_gsm" id="sender_gsm" value="<?php echo esc_attr( $sender_gsm ); ?>" class="regular-text" required /></td>
    837                                 </tr>
    838                                 <tr>
    839                                         <th scope="row"><label for="sender_email"><?php esc_html_e( 'Notify Email', 'express-one-shipment' ); ?></label></th>
    840                                         <td><input type="email" name="sender_email" id="sender_email" value="<?php echo esc_attr( $sender_email ); ?>" class="regular-text" required /></td>
    841                                 </tr>
    842                         </table>
    843                         <?php submit_button( esc_html__( 'Save Settings', 'express-one-shipment' ), 'primary', 'express_one_save_main_settings' ); ?>
    844                 </form>
    845         </div>
    846         <?php
    847 }
    848 
    849 // AJAX handler for layouts
    850 add_action(
    851         'wp_ajax_express_one_fetch_layouts',
    852         function() {
    853                 check_ajax_referer( 'express_one_ajax_nonce', 'nonce' );
    854 
    855                 if ( empty( $_POST['api_key'] ) ) {
    856                         wp_send_json_error( esc_html__( 'API key is required.', 'express-one-shipment' ) );
    857                 }
    858 
    859                 $api_key  = sanitize_text_field( wp_unslash( $_POST['api_key'] ) );
    860                 $response = wp_remote_get(
    861                         'https://api.expressone.si/layouts?apikey=' . rawurlencode( $api_key ),
    862                         [
    863                                 'timeout'   => 15,
    864                                 'sslverify' => true,
    865                         ]
    866                 );
    867 
    868                 if ( is_wp_error( $response ) ) {
    869                         wp_send_json_error( esc_html( $response->get_error_message() ) );
    870                 }
    871 
    872                 $body = wp_remote_retrieve_body( $response );
    873                 $data = json_decode( $body, true );
    874 
    875                 if ( ! isset( $data['data'] ) ) {
    876                         wp_send_json_error( esc_html__( 'API Key is Not Valid.', 'express-one-shipment' ) );
    877                 }
    878 
    879                 $layouts = array_filter(
    880                         $data['data'],
    881                         static function( $item ) {
    882                                 return isset( $item['layoutType'] ) && 0 === (int) $item['layoutType'];
    883                         }
    884                 );
    885 
    886                 wp_send_json_success( array_values( $layouts ) );
    887         }
    888 );
    889 
    890 // Pickup Settings Page
    891 function express_one_pickup_settings_page() {
    892         ?>
    893         <div class="wrap">
    894                 <h1><?php esc_html_e( 'Pickup Point Shipping Settings', 'express-one-shipment' ); ?></h1>
    895 
    896                 <div class="notice notice-info">
    897                         <p><?php esc_html_e( 'Configure pickup point shipping settings below. For API Key, Tokens and Sender information, please visit the main settings page.', 'express-one-shipment' ); ?></p>
    898                 </div>
    899 
    900                 <p>
    901                         <?php
    902                         printf(
    903                 /* translators: %s is the navigation path inside WooCommerce admin */
    904                                 esc_html__( 'To configure Pickup Point shipping method, go to: %s', 'express-one-shipment' ),
    905                                 '<strong>' . esc_html__( 'WooCommerce → Settings → Shipping → Pickup Shipment', 'express-one-shipment' ) . '</strong>'
    906                         );
    907                         ?>
    908                 </p>
    909 
    910                 <p>
    911                         <a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping&section=express_one_pickup_shipping' ) ); ?>"
    912                                 class="button button-primary">
    913                                 <?php esc_html_e( 'Configure Pickup Point Settings', 'express-one-shipment' ); ?>
    914                         </a>
    915                 </p>
    916 
    917                 <hr>
    918 
    919                 <h2><?php esc_html_e( 'Current Configuration', 'express-one-shipment' ); ?></h2>
    920 
    921                 <?php
    922                 $pickup_options = get_option( 'woocommerce_express_one_pickup_shipping_settings', [] );
    923 
    924                 if ( ! empty( $pickup_options ) ) {
    925                         echo '<table class="widefat fixed striped">';
    926                         echo '<thead><tr><th>' . esc_html__( 'Setting', 'express-one-shipment' ) . '</th><th>' . esc_html__( 'Value', 'express-one-shipment' ) . '</th></tr></thead>';
    927                         echo '<tbody>';
    928 
    929                         $setting_labels = [
    930                                 'pickupEnabled'      => esc_html__( 'Enabled', 'express-one-shipment' ),
    931                                 'pickupTitle'        => esc_html__( 'Title', 'express-one-shipment' ),
    932                                 'price'              => esc_html__( 'Delivery Fee', 'express-one-shipment' ),
    933                                 'pickupCodes'        => esc_html__( 'Zip/Post Codes', 'express-one-shipment' ),
    934                                 'pickupAvailability' => esc_html__( 'Availability', 'express-one-shipment' ),
    935                                 'freePickupShipment' => esc_html__( 'Free Shipment Threshold', 'express-one-shipment' ),
    936                         ];
    937 
    938                         foreach ( $setting_labels as $key => $label ) {
    939                                 if ( isset( $pickup_options[ $key ] ) ) {
    940                                         $value = $pickup_options[ $key ];
    941 
    942                                         if ( 'pickupEnabled' === $key ) {
    943                                                 $value = ( 'yes' === $value ) ? esc_html__( 'Yes', 'express-one-shipment' ) : esc_html__( 'No', 'express-one-shipment' );
    944                                         } elseif ( '' === $value ) {
    945                                                 $value = esc_html__( 'Not set', 'express-one-shipment' );
    946                                         }
    947 
    948                                         echo '<tr><td><strong>' . esc_html( $label ) . '</strong></td><td>' . esc_html( $value ) . '</td></tr>';
    949                                 }
    950                         }
    951 
    952                         echo '</tbody></table>';
    953                 } else {
    954                         echo '<p>' . esc_html__( 'No settings configured yet.', 'express-one-shipment' ) . '</p>';
    955                 }
    956                 ?>
    957         </div>
    958         <?php
    959 }
    960 
    961 // Flat Settings Page
    962 function express_one_flat_settings_page() {
    963         ?>
    964         <div class="wrap">
    965                 <h1><?php esc_html_e( 'Home Delivery Shipping Settings', 'express-one-shipment' ); ?></h1>
    966 
    967                 <div class="notice notice-info">
    968                         <p><?php esc_html_e( 'Configure home delivery shipping settings below. For API Key, Tokens and Sender information, please visit the main settings page.', 'express-one-shipment' ); ?></p>
    969                 </div>
    970 
    971                 <p>
    972                         <?php
    973                         printf(
    974                 /* translators: %s is the navigation path inside WooCommerce admin */
    975                                 esc_html__( 'To configure Home Delivery shipping method, go to: %s', 'express-one-shipment' ),
    976                                 '<strong>' . esc_html__( 'WooCommerce → Settings → Shipping → Home Delivery Shipment', 'express-one-shipment' ) . '</strong>'
    977                         );
    978                         ?>
    979                 </p>
    980 
    981                 <p>
    982                         <a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping&section=express_one_flat_shipping' ) ); ?>"
    983                                 class="button button-primary">
    984                                 <?php esc_html_e( 'Configure Home Delivery Settings', 'express-one-shipment' ); ?>
    985                         </a>
    986                 </p>
    987 
    988                 <hr>
    989 
    990                 <h2><?php esc_html_e( 'Current Configuration', 'express-one-shipment' ); ?></h2>
    991 
    992                 <?php
    993                 $flat_options = get_option( 'woocommerce_express_one_flat_shipping_settings', [] );
    994 
    995                 if ( ! empty( $flat_options ) ) {
    996                         echo '<table class="widefat fixed striped">';
    997                         echo '<thead><tr><th>' . esc_html__( 'Setting', 'express-one-shipment' ) . '</th><th>' . esc_html__( 'Value', 'express-one-shipment' ) . '</th></tr></thead>';
    998                         echo '<tbody>';
    999 
    1000                         $setting_labels = [
    1001                                 'flatEnabled'      => esc_html__( 'Enabled', 'express-one-shipment' ),
    1002                                 'flatTitle'        => esc_html__( 'Title', 'express-one-shipment' ),
    1003                                 'price'            => esc_html__( 'Delivery Fee', 'express-one-shipment' ),
    1004                                 'flatCodes'        => esc_html__( 'Zip/Post Codes', 'express-one-shipment' ),
    1005                                 'flatAvailability' => esc_html__( 'Availability', 'express-one-shipment' ),
    1006                                 'freeFlatShipment' => esc_html__( 'Free Shipment Threshold', 'express-one-shipment' ),
    1007                         ];
    1008 
    1009                         foreach ( $setting_labels as $key => $label ) {
    1010                                 if ( isset( $flat_options[ $key ] ) ) {
    1011                                         $value = $flat_options[ $key ];
    1012 
    1013                                         if ( 'flatEnabled' === $key ) {
    1014                                                 $value = ( 'yes' === $value ) ? esc_html__( 'Yes', 'express-one-shipment' ) : esc_html__( 'No', 'express-one-shipment' );
    1015                                         } elseif ( '' === $value ) {
    1016                                                 $value = esc_html__( 'Not set', 'express-one-shipment' );
    1017                                         }
    1018 
    1019                                         echo '<tr><td><strong>' . esc_html( $label ) . '</strong></td><td>' . esc_html( $value ) . '</td></tr>';
    1020                                 }
    1021                         }
    1022 
    1023                         echo '</tbody></table>';
    1024                 } else {
    1025                         echo '<p>' . esc_html__( 'No settings configured yet.', 'express-one-shipment' ) . '</p>';
    1026                 }
    1027                 ?>
    1028         </div>
    1029         <?php
    1030 }
    1031 
    1032 /**
    1033  * Helpers
    1034  */
    1035 function express_one_get_sender_data() {
    1036         return [
    1037                 'name'            => get_option( 'express_one_sender_name', '' ),
    1038                 'countryCode'     => get_option( 'express_one_sender_country', 'SI' ),
    1039                 'zipcode'         => get_option( 'express_one_sender_zipcode', '' ),
    1040                 'city'            => get_option( 'express_one_sender_city', '' ),
    1041                 'streetAndNumber' => get_option( 'express_one_sender_street', '' ),
    1042                 'telephone'       => get_option( 'express_one_sender_telephone', '' ),
    1043                 'fax'             => get_option( 'express_one_sender_fax', '' ),
    1044                 'notifyGSM'       => get_option( 'express_one_sender_gsm', '' ),
    1045                 'notifyEmail'     => get_option( 'express_one_sender_email', '' ),
    1046         ];
    1047 }
    1048 
    1049 function express_one_get_api_key() {
    1050         return get_option( 'express_one_api_key', '' );
    1051 }
    1052 
    1053 function express_one_get_label_layout_id() {
    1054         return get_option( 'express_one_label_layout_id', '' );
    1055 }
    1056 
    1057 /**
    1058  * Distribute order items into a given number of collies (round-robin).
    1059  *
    1060  * @param WC_Order $order        The WooCommerce order.
    1061  * @param int      $collie_count Number of collies to create (clamped to 1…item_count).
    1062  * @return array
    1063  */
    1064 function express_one_build_collies( $order, $collie_count ) {
    1065         $order_id = $order->get_id();
    1066         $items    = array_values( $order->get_items() );
    1067         $total    = count( $items );
    1068 
    1069         $collie_count = max( 1, min( $collie_count, $total ) );
    1070 
    1071         // Initialise empty collies.
    1072         $collies = [];
    1073         for ( $c = 0; $c < $collie_count; $c++ ) {
    1074                 $collies[ $c ] = [
    1075                         'unitAmount' => 0,
    1076                         'ref1'       => 'COLLIE' . ( $c + 1 ) . '_REF',
    1077                         'ref2'       => 'COLLIE' . ( $c + 1 ) . '_ORD' . $order_id,
    1078                         'ref3'       => 'COLLIE' . ( $c + 1 ) . '_INV' . $order_id,
    1079                         'refInt'     => 'COLLIE' . ( $c + 1 ) . '_INT' . $order_id,
    1080                         'remark'     => '',
    1081                         'barcode'    => 'ORDER' . $order_id . '_COLLIE' . ( $c + 1 ),
    1082                 ];
    1083         }
    1084 
    1085         // Round-robin distribute items across collies.
    1086         foreach ( $items as $index => $item ) {
    1087                 $bucket   = $index % $collie_count;
    1088                 $product  = $item->get_product();
    1089                 $quantity = $item->get_quantity();
    1090                 $weight   = $product ? (float) $product->get_weight() : 0;
    1091 
    1092                 $collies[ $bucket ]['unitAmount'] += ( $weight > 0 ? $weight * $quantity : 0 );
    1093 
    1094                 // Append item name to remark.
    1095                 $name = $item->get_name();
    1096                 if ( '' !== $collies[ $bucket ]['remark'] ) {
    1097                         $collies[ $bucket ]['remark'] .= ', ';
    1098                 }
    1099                 $collies[ $bucket ]['remark'] .= $name . ' x' . $quantity;
    1100         }
    1101 
    1102         return array_values( $collies );
    1103 }
    1104 
    1105 /**
    1106  * Shipping Methods: Pickup
    1107  */
    1108 if ( ! class_exists( 'expressOne_Shipping_Options' ) ) {
    1109 
    1110         function express_one_shipping_methods_init() {
    1111 
    1112                 class expressOne_Shipping_Options extends WC_Shipping_Method {
    1113 
    1114                         public function __construct() {
    1115                                 $this->id                  = 'express_one_pickup_shipping';
    1116                                 $this->method_title        = __( 'Pickup Shipment', 'express-one-shipment' );
    1117                                 $this->title               = __( 'Pickup Shipment', 'express-one-shipment' );
    1118                                 $this->options_array_label = 'express_one_shipping_options';
    1119                                 $this->method_description  = __( 'Pickup Shipment Description', 'express-one-shipment' );
    1120 
    1121                                 add_action( 'woocommerce_update_options_shipping_' . $this->id, [ $this, 'process_admin_options' ] );
    1122                                 add_action( 'woocommerce_update_options_shipping_' . $this->id, [ $this, 'process_shipping_options' ] );
    1123                                 $this->init();
    1124                         }
    1125 
    1126                         public function init() {
    1127                                 $this->init_form_fields();
    1128                                 $this->init_settings();
    1129 
    1130                                 $this->pickupEnabled      = $this->get_option( 'pickupEnabled' );
    1131                                 $this->pickupTitle        = __( 'Pickup Shipment', 'express-one-shipment' );
    1132                                 $this->pickupType         = 'fixed';
    1133                                 $this->pickupFee          = $this->get_option( 'price' );
    1134                                 $this->pickupCodes        = $this->get_option( 'pickupCodes' );
    1135                                 $this->pickupAvailability = $this->get_option( 'pickupAvailability' );
    1136                                 $this->pickupCountries    = $this->get_option( 'pickupCountries' );
    1137                                 $this->freePickupShipment = $this->get_option( 'freePickupShipment' );
    1138 
    1139                                 $this->get_shipping_options();
    1140 
    1141                                 add_filter( 'woocommerce_shipping_methods', [ $this, 'add_express_one_shipping_methods' ] );
    1142                                 add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'express_one_field_update_shipping_order_meta' ], 10, 2 );
    1143 
    1144                                 if ( ! is_admin() ) {
    1145                                         add_action( 'woocommerce_cart_shipping_method_full_label', [ $this, 'negative_pickup_label' ], 10, 2 );
    1146                                 }
    1147 
    1148                                 if ( is_admin() ) {
    1149                                         add_action( 'woocommerce_admin_order_data_after_shipping_address', [ $this, 'express_one_display_shipping_admin_order_meta' ], 10, 2 );
    1150                                 }
    1151                         }
    1152 
    1153                         public function calculate_shipping( $package = [] ) {
    1154                                 $shipping_total = 0;
    1155                                 $pickupFee      = ( trim( (string) $this->pickupFee ) === '' ) ? 0 : (float) $this->pickupFee;
    1156                                 $cart_total     = WC()->cart->cart_contents_total;
    1157 
    1158                                 if ( $cart_total >= (float) $this->freePickupShipment ) {
    1159                                         $shipping_total = 0;
    1160                                 } else {
    1161                                         if ( 'fixed' === $this->pickupType ) {
    1162                                                 $shipping_total = $pickupFee;
    1163                                         }
    1164 
    1165                                         if ( 'percent' === $this->pickupType ) {
    1166                                                 $shipping_total = $package['contents_cost'] * ( $pickupFee / 100 );
    1167                                         }
    1168 
    1169                                         if ( 'product' === $this->pickupType ) {
    1170                                                 foreach ( $package['contents'] as $item_id => $values ) {
    1171                                                         $_product = $values['data'];
    1172                                                         if ( $values['quantity'] > 0 && $_product->needs_shipping() ) {
    1173                                                                 $shipping_total += $pickupFee * $values['quantity'];
    1174                                                         }
    1175                                                 }
    1176                                         }
    1177                                 }
    1178 
    1179                                 $shipping_total_with_tax = $shipping_total;
    1180                                 $tax_total               = 0;
    1181                                 $label_suffix            = $this->pickupTitle;
    1182                                 $taxes                   = [];
    1183 
    1184                                 if ( wc_tax_enabled() ) {
    1185                                         $tax_rates = WC_Tax::get_shipping_tax_rates();
    1186                                         $taxes     = WC_Tax::calc_shipping_tax( $shipping_total, $tax_rates );
    1187                                         $tax_total = array_sum( $taxes );
    1188                                         $shipping_total_with_tax += $tax_total;
    1189 
    1190                                         $suffix_template = get_option( 'woocommerce_price_display_suffix', '' );
    1191 
    1192                                         $price_excl_tax = wc_price( $shipping_total );
    1193                                         $price_incl_tax = wc_price( $shipping_total_with_tax );
    1194                                         $tax_amount     = wc_price( $tax_total );
    1195 
    1196                                         $search         = [ '{tax_amount}', '{price_including_tax}', '{price_excluding_tax}' ];
    1197                                         $replace        = [ $tax_amount, $price_incl_tax, $price_excl_tax ];
    1198                                         $dynamic_suffix = str_replace( $search, $replace, $suffix_template );
    1199 
    1200                                         if ( ! empty( $dynamic_suffix ) ) {
    1201                                                 $label_suffix .= ' (' . $dynamic_suffix . ')';
    1202                                         }
    1203                                 }
    1204 
    1205                                 $rate = [
    1206                                         'id'    => $this->id,
    1207                                         'label' => $label_suffix,
    1208                                         'cost'  => $shipping_total,
    1209                                         'taxes' => $taxes,
    1210                                 ];
    1211 
    1212                                 $this->add_rate( $rate );
    1213                         }
    1214 
    1215                         public function init_form_fields() {
    1216                                 $this->form_fields = [
    1217                                         'pickupEnabled'      => [
    1218                                                 'title'   => esc_html__( 'Enable', 'express-one-shipment' ),
    1219                                                 'type'    => 'checkbox',
    1220                                                 'label'   => esc_html__( 'Enable Pickup Point', 'express-one-shipment' ),
    1221                                                 'default' => 'no',
    1222                                         ],
    1223                                         'pickupCodes'        => [
    1224                                                 'title'       => esc_html__( 'Zip/Post Codes', 'express-one-shipment' ),
    1225                                                 'type'        => 'textarea',
    1226                                                 'description' => esc_html__( 'What zip/post codes can use the local pickup option? Separate codes with a comma. Accepts wildcards, e.g. P* will match a postcode of PE30.', 'express-one-shipment' ),
    1227                                                 'default'     => '',
    1228                                                 'desc_tip'    => true,
    1229                                                 'placeholder' => '12345, 56789, etc',
    1230                                         ],
    1231                                         'price'              => [
    1232                                                 'title'       => esc_html__( 'Pickup Point Delivery Fee/Discount', 'express-one-shipment' ),
    1233                                                 'type'        => 'price',
    1234                                                 'description' => esc_html__( 'What fee do you want to charge for local pickup? Enter negative value for discount. Leave blank to disable.', 'express-one-shipment' ),
    1235                                                 'default'     => '',
    1236                                                 'desc_tip'    => true,
    1237                                                 'placeholder' => wc_format_localized_price( 0 ),
    1238                                         ],
    1239                                         'pickupAvailability' => [
    1240                                                 'title'   => esc_html__( 'Method availability', 'express-one-shipment' ),
    1241                                                 'type'    => 'select',
    1242                                                 'default' => 'all',
    1243                                                 'class'   => 'availability',
    1244                                                 'options' => [
    1245                                                         'all'      => esc_html__( 'All allowed countries', 'express-one-shipment' ),
    1246                                                         'specific' => esc_html__( 'Specific Countries', 'express-one-shipment' ),
    1247                                                 ],
    1248                                         ],
    1249                                         'pickupCountries'    => [
    1250                                                 'title'             => esc_html__( 'Specific Countries', 'express-one-shipment' ),
    1251                                                 'type'              => 'multiselect',
    1252                                                 'class'             => 'chosen_select',
    1253                                                 'css'               => 'width: 450px;',
    1254                                                 'default'           => '',
    1255                                                 'options'           => function_exists( 'WC' ) && WC()->countries ? WC()->countries->get_shipping_countries() : [],
    1256                                                 'custom_attributes' => [
    1257                                                         'data-placeholder' => esc_html__( 'Select some countries', 'express-one-shipment' ),
    1258                                                 ],
    1259                                         ],
    1260                                         'freePickupShipment' => [
    1261                                                 'title'       => esc_html__( 'Custom Free Shipment', 'express-one-shipment' ),
    1262                                                 'type'        => 'price',
    1263                                                 'description' => esc_html__( 'Enter the users price. For example if it is greater than $50, shipping will be free.', 'express-one-shipment' ),
    1264                                                 'default'     => '',
    1265                                                 'desc_tip'    => true,
    1266                                         ],
    1267                                 ];
    1268                         }
    1269 
    1270                         public function admin_options() {
    1271                                 ?>
    1272                                 <h3><?php echo esc_html( $this->method_title ); ?></h3>
    1273                                 <p><?php esc_html_e( 'Pickup Shipment Description', 'express-one-shipment' ); ?></p>
    1274                                 <table class="form-table">
    1275                                         <?php $this->generate_settings_html(); ?>
    1276                                 </table>
    1277                                 <?php
    1278                         }
    1279 
    1280                         public function is_available( $package ) {
    1281                                 $pickup_enabled = ( 'no' === $this->pickupEnabled );
    1282                                 if ( $pickup_enabled ) {
    1283                                         return false;
    1284                                 }
    1285 
    1286                                 $pickupCodes = [];
    1287                                 if ( ! empty( $this->pickupCodes ) ) {
    1288                                         foreach ( explode( ',', $this->pickupCodes ) as $code ) {
    1289                                                 $pickupCodes[] = $this->clean( $code );
    1290                                         }
    1291                                 }
    1292 
    1293                                 if ( is_array( $pickupCodes ) && ! empty( $pickupCodes ) ) {
    1294                                         $found_match = false;
    1295 
    1296                                         if ( in_array( $this->clean( $package['destination']['postcode'] ), $pickupCodes, true ) ) {
    1297                                                 $found_match = true;
    1298                                         }
    1299 
    1300                                         if ( ! $found_match ) {
    1301                                                 $customer_postcode = $this->clean( $package['destination']['postcode'] );
    1302                                                 foreach ( $pickupCodes as $c ) {
    1303                                                         $pattern = '/^' . str_replace( '_', '[0-9a-zA-Z]', preg_quote( $c, '/' ) ) . '$/i';
    1304                                                         if ( preg_match( $pattern, $customer_postcode ) ) {
    1305                                                                 $found_match = true;
    1306                                                                 break;
    1307                                                         }
    1308                                                 }
    1309                                         }
    1310 
    1311                                         if ( ! $found_match ) {
    1312                                                 $customer_postcode        = $this->clean( $package['destination']['postcode'] );
    1313                                                 $customer_postcode_length = strlen( $customer_postcode );
    1314 
    1315                                                 for ( $i = 0; $i <= $customer_postcode_length; $i++ ) {
    1316                                                         if ( in_array( $customer_postcode, $pickupCodes, true ) ) {
    1317                                                                 $found_match = true;
    1318                                                         }
    1319                                                         $customer_postcode = substr( $customer_postcode, 0, -2 ) . '*';
    1320                                                 }
    1321                                         }
    1322 
    1323                                         if ( ! $found_match ) {
    1324                                                 return false;
    1325                                         }
    1326                                 }
    1327 
    1328                                 if ( 'specific' === $this->pickupAvailability ) {
    1329                                         $ship_to_countries = $this->pickupCountries;
    1330                                 } else {
    1331                                         $ship_to_countries = function_exists( 'WC' ) && WC()->countries ? array_keys( WC()->countries->get_shipping_countries() ) : [];
    1332                                 }
    1333 
    1334                                 if ( is_array( $ship_to_countries ) && ! empty( $ship_to_countries ) ) {
    1335                                         if ( ! in_array( $package['destination']['country'], $ship_to_countries, true ) ) {
    1336                                                 return false;
    1337                                         }
    1338                                 }
    1339 
    1340                                 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
    1341                                 return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', true, $package );
    1342                         }
    1343 
    1344                         public function clean( $code ) {
    1345                                 return str_replace( '-', '', sanitize_title( $code ) ) . ( strstr( $code, '*' ) ? '*' : '' );
    1346                         }
    1347 
    1348                         public function validate_shipping_options_table_field( $key ) { // phpcs:ignore
    1349                                 return false;
    1350                         }
    1351 
    1352                         public function process_shipping_options() {
    1353                                 $options = [];
    1354 
    1355                                 $nonce = isset( $_POST[ $this->id . '_options_nonce' ] )
    1356                                         ? sanitize_text_field( wp_unslash( $_POST[ $this->id . '_options_nonce' ] ) )
    1357                                         : '';
    1358 
    1359                                 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $this->id . '_save_options' ) ) {
    1360                                         return;
    1361                                 }
    1362 
    1363                                 $key = $this->id . '_options';
    1364 
    1365                                 if ( isset( $_POST[ $key ] ) ) {
    1366                                         // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    1367                                         $raw = wp_unslash( $_POST[ $key ] );
    1368 
    1369                                         if ( is_array( $raw ) ) {
    1370                                                 $options = map_deep( $raw, 'wc_clean' );
    1371                                         }
    1372                                 }
    1373 
    1374                                 update_option( $this->options_array_label, $options );
    1375                                 $this->get_shipping_options();
    1376                         }
    1377 
    1378                         public function get_shipping_options() {
    1379                                 $this->shipping_options = array_filter( (array) get_option( $this->options_array_label ) );
    1380                         }
    1381 
    1382                         public function express_one_field_update_shipping_order_meta( $order_id, $posted ) {
    1383 
    1384                                 $nonce = isset( $_POST['woocommerce-process-checkout-nonce'] )
    1385                                         ? sanitize_text_field( wp_unslash( $_POST['woocommerce-process-checkout-nonce'] ) )
    1386                                         : '';
    1387 
    1388                                 if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'woocommerce-process_checkout' ) ) {
    1389                                         return;
    1390                                 }
    1391 
    1392                                 $order = wc_get_order( $order_id );
    1393                                 if ( ! $order ) {
    1394                                         return;
    1395                                 }
    1396 
    1397                                 if ( is_array( $posted['shipping_method'] ) && in_array( $this->id, $posted['shipping_method'], true ) ) {
    1398                                         if ( isset( $_POST['shipping_option'] ) && '' !== $_POST['shipping_option'] ) {
    1399                                                 $shipping_option = sanitize_text_field( wp_unslash( $_POST['shipping_option'] ) );
    1400                                                 $order->update_meta_data( 'express_one_shipping_option', $shipping_option );
    1401                                                 $order->save();
    1402 
    1403                                                 if ( WC()->session ) {
    1404                                                         WC()->session->_chosen_shipping_option = $shipping_option; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
    1405                                                 }
    1406                                         }
    1407                                 } else {
    1408                                         if ( WC()->session ) {
    1409                                                 $chosen_method = WC()->session->get( 'chosen_shipping_methods' );
    1410                                                 $chosen_option = WC()->session->_chosen_shipping_option ?? ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
    1411                                                 if ( is_array( $chosen_method ) && in_array( $this->id, $chosen_method, true ) && $chosen_option ) {
    1412                                                         $order->update_meta_data( 'express_one_shipping_option', sanitize_text_field( $chosen_option ) );
    1413                                                         $order->save();
    1414                                                 }
    1415                                         }
    1416                                 }
    1417                         }
    1418 
    1419                         public function express_one_display_shipping_admin_order_meta( $order ) {
    1420                                 $selected_option = $order->get_meta( 'express_one_shipping_option', true );
    1421                                 if ( $selected_option ) {
    1422                                         echo '<p><strong>' . esc_html( $this->pickupTitle ) . ':</strong> ' . esc_html( $selected_option ) . '</p>';
    1423                                 }
    1424                         }
    1425 
    1426                         public function add_express_one_shipping_methods( $methods ) {
    1427                                 $methods[] = $this;
    1428                                 return $methods;
    1429                         }
    1430 
    1431                         public function negative_pickup_label( $label, $method ) {
    1432                                 if ( ! is_admin() && 'express_one_pickup_shipping' === $method->id ) {
    1433                                         $label = $method->label . ' : ' . wc_price( $method->cost );
    1434                                 }
    1435                                 return $label;
    1436                         }
    1437                 }
    1438 
    1439                 new expressOne_Shipping_Options();
    1440         }
    1441 
    1442         add_action( 'woocommerce_shipping_init', 'express_one_shipping_methods_init' );
    1443 
    1444         /**
    1445          * AJAX: save selected option
    1446          */
    1447         add_action( 'wp_ajax_express_one_save_selected', 'express_one_save_selected_option' );
    1448         add_action( 'wp_ajax_nopriv_express_one_save_selected', 'express_one_save_selected_option' );
    1449         function express_one_save_selected_option() {
    1450                 $nonce = isset( $_REQUEST['nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ) : '';
    1451                 if ( ! wp_verify_nonce( $nonce, 'express_one_pickup_nonce' ) ) {
    1452                         wp_die( esc_html__( 'Invalid nonce.', 'express-one-shipment' ) );
    1453                 }
    1454 
    1455                 if ( isset( $_GET['shipping_option'] ) && ! empty( $_GET['shipping_option'] ) ) {
    1456                         $selected_option = sanitize_text_field( wp_unslash( $_GET['shipping_option'] ) );
    1457 
    1458                         $allowed = [ 'pickup', 'flat' ];
    1459                         if ( ! in_array( $selected_option, $allowed, true ) ) {
    1460                                 wp_die( esc_html__( 'Invalid selection.', 'express-one-shipment' ) );
    1461                         }
    1462                         if ( WC()->session ) {
    1463                                 WC()->session->_chosen_shipping_option = $selected_option; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
    1464                         }
    1465                 }
    1466                 wp_die();
    1467         }
    1468 
    1469         /**
    1470          * Admin order buttons (Create/Update/Print) with collie count input
    1471          */
    1472         add_action( 'woocommerce_admin_order_data_after_order_details', 'express_one_add_order_buttons' );
    1473         function express_one_add_order_buttons( $order ) {
    1474                 $order_id = $order->get_id();
    1475 
    1476                 // HPOS-compatible: use order CRUD instead of get_post_meta.
    1477                 $shipment_id = $order->get_meta( '_expressone_shipment_id', true );
    1478                 $cargo_ids   = $order->get_meta( '_expressone_cargo_ids', true );
    1479 
    1480                 if ( empty( $cargo_ids ) ) {
    1481                         $cargo_ids = [];
    1482                 } elseif ( is_string( $cargo_ids ) ) {
    1483                         $maybe_unserialized = maybe_unserialize( $cargo_ids );
    1484                         if ( is_array( $maybe_unserialized ) ) {
    1485                                 $cargo_ids = $maybe_unserialized;
    1486                         } else {
    1487                                 $maybe_json = json_decode( $cargo_ids, true );
    1488                                 $cargo_ids  = is_array( $maybe_json ) ? $maybe_json : [ $cargo_ids ];
    1489                         }
    1490                 } elseif ( ! is_array( $cargo_ids ) ) {
    1491                         $cargo_ids = (array) $cargo_ids;
    1492                 }
    1493 
    1494                 // Count order items for max collie validation.
    1495                 $item_count = count( $order->get_items() );
    1496                 if ( $item_count < 1 ) {
    1497                         $item_count = 1;
    1498                 }
    1499 
    1500                 if ( $shipment_id ) {
    1501                         $update_url = wp_nonce_url(
    1502                                 admin_url( 'admin-post.php?action=update_expressone_shipment&order_id=' . (int) $order_id ),
    1503                                 'express_one_update_shipment_' . (int) $order_id,
    1504                                 '_wpnonce'
    1505                         );
    1506 
    1507                         $print_label_url = wp_nonce_url(
    1508                                 admin_url( 'admin-post.php?action=print_expressone_label&order_id=' . (int) $order_id ),
    1509                                 'express_one_print_label_' . (int) $order_id,
    1510                                 '_wpnonce'
    1511                         );
    1512 
    1513                         $first_cargo_id = ! empty( $cargo_ids ) ? reset( $cargo_ids ) : '';
    1514 
    1515                         echo '<p style="margin-top:10px;">';
    1516                         echo '<a href="' . esc_url( $update_url ) . '" id="express-one-update-btn" data-max-collies="' . esc_attr( $item_count ) . '" style="margin-right: 10px; margin-top:15px" class="button button-primary">' . esc_html__( 'Update Shipment', 'express-one-shipment' ) . '</a>';
    1517 
    1518                         if ( ! empty( $first_cargo_id ) ) {
    1519                                 $tracking_url = 'https://inet.expressone.si/auth?id=' . rawurlencode( (string) $first_cargo_id );
    1520                                 echo ' <a href="' . esc_url( $tracking_url ) . '" target="_blank" rel="noopener noreferrer" style="margin-right: 10px; margin-top:15px" class="button button-secondary">' . esc_html__( 'Show Tracking', 'express-one-shipment' ) . '</a>';
    1521                         } else {
    1522                                 echo ' <button type="button" style="margin-right: 10px; margin-top:15px" class="button" disabled title="' . esc_attr__( 'No tracking ID found', 'express-one-shipment' ) . '">' . esc_html__( 'Show Tracking', 'express-one-shipment' ) . '</button>';
    1523                         }
    1524 
    1525                         echo ' <a href="' . esc_url( $print_label_url ) . '" target="_blank" rel="noopener noreferrer" style="margin-right:10px; margin-top:15px" class="button">' . esc_html__( 'Print Label', 'express-one-shipment' ) . '</a>';
    1526                         echo '</p>';
    1527 
    1528                 } else {
    1529                         $create_url = wp_nonce_url(
    1530                                 admin_url( 'admin-post.php?action=create_express_one_shipment&order_id=' . (int) $order_id ),
    1531                                 'express_one_create_shipment_' . (int) $order_id,
    1532                                 '_wpnonce'
    1533                         );
    1534 
    1535                         echo '<p style="margin-top:10px;">';
    1536                         echo '<a href="' . esc_url( $create_url ) . '" id="express-one-create-btn" style="margin-right: 10px; margin-top:15px" data-max-collies="' . esc_attr( $item_count ) . '" class="button button-primary">' . esc_html__( 'Create Express One Shipment', 'express-one-shipment' ) . '</a>';
    1537                         echo '</p>';
    1538                 }
    1539         }
    1540 
    1541         /**
    1542          * HPOS-compatible error redirect for admin order screens.
    1543          */
    1544         function express_one_admin_error_redirect( $order_id, $message ) {
    1545                 set_transient( 'express_one_shipment_error_' . (int) $order_id, $message, 60 );
    1546                 $url = express_one_get_order_edit_url( $order_id );
    1547                 wp_safe_redirect( $url );
    1548                 exit;
    1549         }
    1550 
    1551         add_action( 'admin_post_create_express_one_shipment', 'express_one_handle_create_shipment' );
    1552         function express_one_handle_create_shipment() {
    1553                 if ( ! current_user_can( 'manage_woocommerce' ) ) {
    1554                         wp_die( esc_html__( 'Unauthorized', 'express-one-shipment' ) );
    1555                 }
    1556 
    1557                 $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
    1558                 if ( ! $order_id ) {
    1559                         express_one_admin_error_redirect( 0, 'Invalid order ID.' );
    1560                 }
    1561 
    1562                 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
    1563                 if ( ! wp_verify_nonce( $nonce, 'express_one_create_shipment_' . $order_id ) ) {
    1564                         wp_die( esc_html__( 'Security check failed.', 'express-one-shipment' ) );
    1565                 }
    1566 
    1567                 $order = wc_get_order( $order_id );
    1568                 if ( ! $order ) {
    1569                         express_one_admin_error_redirect( $order_id, 'Order not found.' );
    1570                 }
    1571 
    1572                 $api_key     = express_one_get_api_key();
    1573                 $sender_data = express_one_get_sender_data();
    1574 
    1575                 if ( empty( $api_key ) ) {
    1576                         express_one_admin_error_redirect( $order_id, 'Missing API Key. Please configure it in Express One Settings.' );
    1577                 }
    1578 
    1579                 if ( empty( $sender_data['name'] ) ) {
    1580                         express_one_admin_error_redirect( $order_id, 'Missing Sender Information. Please configure it in Express One Settings.' );
    1581                 }
    1582 
    1583                 $cod_value = 0;
    1584                 if ( 'cod' === $order->get_payment_method() ) {
    1585                         $cod_value = (float) $order->get_total();
    1586                 }
    1587 
    1588                 // Build collies based on the requested count.
    1589                 $item_count   = count( $order->get_items() );
    1590                 $collie_count = isset( $_GET['collie_count'] ) ? absint( $_GET['collie_count'] ) : $item_count;
    1591                 $collie_count = max( 1, min( $collie_count, $item_count ) );
    1592 
    1593                 $collies = express_one_build_collies( $order, $collie_count );
    1594 
    1595                 $body = [
    1596                         'cosignee'            => [
    1597                                 'name'            => trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() ),
    1598                                 'countryCode'     => $order->get_shipping_country(),
    1599                                 'zipcode'         => $order->get_shipping_postcode(),
    1600                                 'city'            => $order->get_shipping_city(),
    1601                                 'streetAndNumber' => $order->get_shipping_address_1(),
    1602                                 'telephone'       => $order->get_billing_phone(),
    1603                                 'fax'             => '',
    1604                                 'notifyGSM'       => $order->get_billing_phone(),
    1605                                 'notifyEmail'     => $order->get_billing_email(),
    1606                         ],
    1607                         'sender'              => $sender_data,
    1608                         'cosigneeNotifyType'  => 'Email',
    1609                         'isSenderNonCustomer' => false,
    1610                         'unitAmount'          => array_sum( wp_list_pluck( $collies, 'unitAmount' ) ),
    1611                         'ref1'                => 'REF' . $order_id,
    1612                         'ref2'                => 'ORD' . $order->get_order_number(),
    1613                         'ref3'                => 'INV' . $order_id,
    1614                         'refInt'              => 'INT' . $order_id,
    1615                         'exWorksType'         => 1,
    1616                         'cargo'               => false,
    1617                         'returnDocument'      => false,
    1618                         'remark'              => 'Handle with care',
    1619                         'codValue'            => $cod_value,
    1620                         'codCurrency'         => 0,
    1621                         'collies'             => $collies,
    1622                         'noSticker'           => false,
    1623                 ];
    1624 
    1625                 $api_url  = 'https://api.expressone.si/createshipment?apikey=' . rawurlencode( $api_key );
    1626                 $response = wp_remote_post(
    1627                         $api_url,
    1628                         [
    1629                                 'headers' => [ 'Content-Type' => 'application/json' ],
    1630                                 'body'    => wp_json_encode( $body ),
    1631                                 'timeout' => 30,
    1632                         ]
    1633                 );
    1634 
    1635                 if ( is_wp_error( $response ) ) {
    1636                         express_one_admin_error_redirect( $order_id, 'Shipment API error: ' . $response->get_error_message() );
    1637                 }
    1638 
    1639                 $status_code   = wp_remote_retrieve_response_code( $response );
    1640                 $response_body = wp_remote_retrieve_body( $response );
    1641                 $response_data = json_decode( $response_body, true );
    1642 
    1643                 if ( $status_code >= 200 && $status_code < 300 && ! empty( $response_data['shipmentid'] ) ) {
    1644                         // HPOS-compatible: use order CRUD instead of update_post_meta.
    1645                         $order->update_meta_data( '_expressone_shipment_id', sanitize_text_field( (string) $response_data['shipmentid'] ) );
    1646 
    1647                         if ( ! empty( $response_data['CargoIDs'] ) ) {
    1648                                 $cargo_ids = is_array( $response_data['CargoIDs'] ) ? $response_data['CargoIDs'] : [ $response_data['CargoIDs'] ];
    1649                                 $cargo_ids = array_map(
    1650                                         static function( $v ) {
    1651                                                 return sanitize_text_field( (string) $v );
    1652                                         },
    1653                                         $cargo_ids
    1654                                 );
    1655                                 $order->update_meta_data( '_expressone_cargo_ids', $cargo_ids );
    1656                         }
    1657 
    1658                         $order->save();
    1659 
    1660                         $order->update_status(
    1661                                 'completed',
    1662                                 esc_html__( 'Shipment created successfully and order marked as completed.', 'express-one-shipment' )
    1663                         );
    1664                         set_transient( 'express_one_shipment_created_' . $order_id, 1, 60 );
    1665 
    1666                         $redirect_url = express_one_get_order_edit_url( $order_id );
    1667                         wp_safe_redirect( $redirect_url );
    1668                         exit;
    1669                 } else {
    1670                         $error_msg = ! empty( $response_data['error']['message'] ?? '' ) ? (string) $response_data['error']['message'] : (string) $response_body;
    1671 
    1672                         set_transient( 'express_one_shipment_error_' . $order_id, $error_msg, 60 );
    1673 
    1674                         $redirect_url = express_one_get_order_edit_url( $order_id );
    1675                         wp_safe_redirect( $redirect_url );
    1676                         exit;
    1677                 }
    1678 
    1679                 exit;
    1680         }
    1681 
    1682         add_action( 'admin_post_update_expressone_shipment', 'express_one_handle_update_shipment' );
    1683         function express_one_handle_update_shipment() {
    1684                 if ( ! current_user_can( 'manage_woocommerce' ) ) {
    1685                         wp_die( esc_html__( 'Unauthorized', 'express-one-shipment' ) );
    1686                 }
    1687 
    1688                 $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
    1689                 if ( ! $order_id ) {
    1690                         wp_die( esc_html__( 'Invalid order ID', 'express-one-shipment' ) );
    1691                 }
    1692 
    1693                 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
    1694                 if ( ! wp_verify_nonce( $nonce, 'express_one_update_shipment_' . $order_id ) ) {
    1695                         wp_die( esc_html__( 'Security check failed.', 'express-one-shipment' ) );
    1696                 }
    1697 
    1698                 $order = wc_get_order( $order_id );
    1699                 if ( ! $order ) {
    1700                         wp_die( esc_html__( 'Order not found', 'express-one-shipment' ) );
    1701                 }
    1702 
    1703                 $api_key     = express_one_get_api_key();
    1704                 $sender_data = express_one_get_sender_data();
    1705 
    1706                 // HPOS-compatible: use order CRUD instead of get_post_meta.
    1707                 $shipment_id = $order->get_meta( '_expressone_shipment_id', true );
    1708 
    1709                 if ( ! $shipment_id ) {
    1710                         wp_die( esc_html__( 'Shipment not found for this order', 'express-one-shipment' ) );
    1711                 }
    1712 
    1713                 $cod_value = 0;
    1714                 if ( 'cod' === $order->get_payment_method() ) {
    1715                         $cod_value = (float) $order->get_total();
    1716                 }
    1717 
    1718                 // Build collies based on the requested count.
    1719                 $item_count   = count( $order->get_items() );
    1720                 $collie_count = isset( $_GET['collie_count'] ) ? absint( $_GET['collie_count'] ) : $item_count;
    1721                 $collie_count = max( 1, min( $collie_count, $item_count ) );
    1722 
    1723                 $collies = express_one_build_collies( $order, $collie_count );
    1724 
    1725                 $body = [
    1726                         'cosignee'            => [
    1727                                 'name'            => trim( $order->get_shipping_first_name() . ' ' . $order->get_shipping_last_name() ),
    1728                                 'countryCode'     => $order->get_shipping_country(),
    1729                                 'zipcode'         => $order->get_shipping_postcode(),
    1730                                 'city'            => $order->get_shipping_city(),
    1731                                 'streetAndNumber' => $order->get_shipping_address_1(),
    1732                                 'telephone'       => $order->get_billing_phone(),
    1733                                 'fax'             => '',
    1734                                 'notifyGSM'       => $order->get_billing_phone(),
    1735                                 'notifyEmail'     => $order->get_billing_email(),
    1736                         ],
    1737                         'sender'              => $sender_data,
    1738                         'cosigneeNotifyType'  => 'Email',
    1739                         'isSenderNonCustomer' => false,
    1740                         'unitAmount'          => array_sum( wp_list_pluck( $collies, 'unitAmount' ) ),
    1741                         'ref1'                => 'REF' . $order_id,
    1742                         'ref2'                => 'ORD' . $order->get_order_number(),
    1743                         'ref3'                => 'INV' . $order_id,
    1744                         'refInt'              => 'INT' . $order_id,
    1745                         'exWorksType'         => 1,
    1746                         'cargo'               => true,
    1747                         'returnDocument'      => true,
    1748                         'remark'              => esc_html__( 'Handle with care', 'express-one-shipment' ),
    1749                         'codValue'            => $cod_value,
    1750                         'codCurrency'         => 0,
    1751                         'collies'             => $collies,
    1752                         'noSticker'           => false,
    1753                 ];
    1754 
    1755                 $api_url = 'https://api.expressone.si/updateshipment?shipmentId=' . rawurlencode( (string) $shipment_id ) . '&apikey=' . rawurlencode( $api_key );
    1756 
    1757                 $response = wp_remote_post(
    1758                         $api_url,
    1759                         [
    1760                                 'headers' => [ 'Content-Type' => 'application/json' ],
    1761                                 'body'    => wp_json_encode( $body ),
    1762                                 'timeout' => 30,
    1763                         ]
    1764                 );
    1765 
    1766                 $redirect_url = express_one_get_order_edit_url( $order_id );
    1767 
    1768                 if ( is_wp_error( $response ) ) {
    1769                         set_transient( 'express_one_shipment_error_' . $order_id, $response->get_error_message(), 60 );
    1770                         wp_safe_redirect( $redirect_url );
    1771                         exit;
    1772                 }
    1773 
    1774                 $status_code   = wp_remote_retrieve_response_code( $response );
    1775                 $response_body = wp_remote_retrieve_body( $response );
    1776 
    1777                 if ( $status_code >= 200 && $status_code < 300 ) {
    1778                         set_transient( 'express_one_shipment_updated_' . $order_id, 1, 60 );
    1779                         wp_safe_redirect( $redirect_url );
    1780                         exit;
    1781                 } else {
    1782                         set_transient( 'express_one_shipment_error_' . $order_id, (string) $response_body, 60 );
    1783                         wp_safe_redirect( $redirect_url );
    1784                         exit;
    1785                 }
    1786 
    1787                 exit;
    1788         }
    1789 
    1790         add_action( 'admin_post_print_expressone_label', 'express_one_handle_print_label' );
    1791         function express_one_handle_print_label() {
    1792                 if ( ! current_user_can( 'manage_woocommerce' ) ) {
    1793                         wp_die( esc_html__( 'Unauthorized', 'express-one-shipment' ), 'Error', [ 'response' => 403 ] );
    1794                 }
    1795 
    1796                 $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
    1797                 if ( ! $order_id ) {
    1798                         wp_die( esc_html__( 'Invalid order ID', 'express-one-shipment' ) );
    1799                 }
    1800 
    1801                 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
    1802                 if ( ! wp_verify_nonce( $nonce, 'express_one_print_label_' . $order_id ) ) {
    1803                         wp_die( esc_html__( 'Security check failed.', 'express-one-shipment' ) );
    1804                 }
    1805 
    1806                 // HPOS-compatible: use order CRUD instead of get_post_meta.
    1807                 $order = wc_get_order( $order_id );
    1808                 if ( ! $order ) {
    1809                         wp_die( esc_html__( 'Order not found', 'express-one-shipment' ) );
    1810                 }
    1811 
    1812                 $shipment_id = $order->get_meta( '_expressone_shipment_id', true );
    1813                 if ( ! $shipment_id ) {
    1814                         wp_die( esc_html__( 'No shipment found for this order', 'express-one-shipment' ) );
    1815                 }
    1816 
    1817                 $api_key = express_one_get_api_key();
    1818                 if ( empty( $api_key ) ) {
    1819                         wp_die( esc_html__( 'API Key is missing. Please configure it in Express One settings.', 'express-one-shipment' ) );
    1820                 }
    1821 
    1822                 $label_layout_id = express_one_get_label_layout_id();
    1823                 if ( empty( $label_layout_id ) ) {
    1824                         wp_die( esc_html__( 'Label Layout ID is missing. Please select a label layout in Express One settings.', 'express-one-shipment' ) );
    1825                 }
    1826 
    1827                 $api_url = 'https://api.expressone.si/pdfinternal?layoutid=' . rawurlencode( $label_layout_id ) . '&apikey=' . rawurlencode( $api_key );
    1828                 $body    = wp_json_encode( [ (int) $shipment_id ] );
    1829 
    1830                 $response = wp_remote_post(
    1831                         $api_url,
    1832                         [
    1833                                 'headers' => [ 'Content-Type' => 'application/json' ],
    1834                                 'body'    => $body,
    1835                                 'timeout' => 30,
    1836                         ]
    1837                 );
    1838 
    1839                 if ( is_wp_error( $response ) ) {
    1840                         wp_die( esc_html__( 'API request failed: ', 'express-one-shipment' ) . esc_html( $response->get_error_message() ) );
    1841                 }
    1842 
    1843                 $response_body = wp_remote_retrieve_body( $response );
    1844                 $data          = json_decode( $response_body, true );
    1845 
    1846                 if ( empty( $data['labelsbase64'] ) ) {
    1847                         $msg = esc_html__( 'No PDF data received from API.', 'express-one-shipment' ) . '<br><pre>' . esc_html( (string) $response_body ) . '</pre>';
    1848                         wp_die( wp_kses_post( $msg ) );
    1849                 }
    1850 
    1851                 $decoded_pdf = base64_decode( (string) $data['labelsbase64'] );
    1852                 if ( ! $decoded_pdf ) {
    1853                         wp_die( esc_html__( 'Failed to decode PDF data.', 'express-one-shipment' ) );
    1854                 }
    1855 
    1856                 header( 'Content-Type: application/pdf' );
    1857                 header( 'Content-Disposition: attachment; filename="shipment-label-' . (int) $shipment_id . '.pdf"' );
    1858                 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary data must be output raw.
    1859                 echo $decoded_pdf;
    1860                 exit;
    1861         }
    1862 
    1863         /**
    1864          * Orders list action: Print Label
    1865          */
    1866         add_filter(
    1867                 'woocommerce_admin_order_actions',
    1868                 function( $actions, $order ) {
    1869                         // HPOS-compatible: use order CRUD instead of get_post_meta.
    1870                         $shipment_id = $order->get_meta( '_expressone_shipment_id', true );
    1871 
    1872                         if ( $shipment_id ) {
    1873                                 $url = wp_nonce_url(
    1874                                         admin_url( 'admin-post.php?action=print_expressone_label&order_id=' . (int) $order->get_id() ),
    1875                                         'express_one_print_label_' . (int) $order->get_id(),
    1876                                         '_wpnonce'
    1877                                 );
    1878 
    1879                                 $actions['print_expressone_label'] = [
    1880                                         'url'    => $url,
    1881                                         'name'   => esc_html__( 'Print Label', 'express-one-shipment' ),
    1882                                         'action' => 'download_label',
    1883                                         'target' => '_blank',
    1884                                 ];
    1885                         }
    1886 
    1887                         return $actions;
    1888                 },
    1889                 10,
    1890                 2
    1891         );
    1892 
    1893         /**
    1894          * Bulk labels
    1895          */
    1896         add_filter( 'bulk_actions-woocommerce_page_wc-orders', 'express_one_bulk_labels_bulk_actions' );
    1897         function express_one_bulk_labels_bulk_actions( $bulk_actions ) {
    1898                 $bulk_actions['print_bulk_labels'] = esc_html__( 'Print Labels', 'express-one-shipment' );
    1899                 return $bulk_actions;
    1900         }
    1901 
    1902         add_filter(
    1903                 'handle_bulk_actions-woocommerce_page_wc-orders',
    1904                 function( $redirect_to, $action, $post_ids ) {
    1905                         if ( 'print_bulk_labels' !== $action ) {
    1906                                 return $redirect_to;
    1907                         }
    1908 
    1909                         // Some WooCommerce/HPOS versions pass the selected IDs under a different
    1910                         // request key than the one already parsed into $post_ids. Fall back to
    1911                         // reading the raw request so the action never silently no-ops.
    1912                         if ( empty( $post_ids ) ) {
    1913                                 $raw_ids = [];
    1914                                 foreach ( [ 'id', 'order', 'post' ] as $request_key ) {
    1915                                         if ( ! empty( $_REQUEST[ $request_key ] ) && is_array( $_REQUEST[ $request_key ] ) ) {
    1916                                                 $raw_ids = wp_unslash( $_REQUEST[ $request_key ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce already verified by the bulk action framework.
    1917                                                 break;
    1918                                         }
    1919                                 }
    1920                                 $post_ids = array_map( 'absint', $raw_ids );
    1921                         }
    1922 
    1923                         if ( empty( $post_ids ) ) {
    1924                                 set_transient(
    1925                                         'bulk_labels_result_' . get_current_user_id(),
    1926                                         [
    1927                                                 'success' => [],
    1928                                                 'failed'  => [],
    1929                                                 'pdf_url' => '',
    1930                                                 'error'   => esc_html__( 'No orders were selected. Please select one or more orders before applying this action.', 'express-one-shipment' ),
    1931                                         ],
    1932                                         60
    1933                                 );
    1934                                 return wp_get_referer();
    1935                         }
    1936 
    1937                         $shipment_ids   = [];
    1938                         $failed_orders  = [];
    1939                         $success_orders = [];
    1940 
    1941                         foreach ( $post_ids as $order_id ) {
    1942                                 $order_id = (int) $order_id;
    1943                                 $order    = wc_get_order( $order_id );
    1944 
    1945                                 if ( ! $order ) {
    1946                                         $failed_orders[] = $order_id;
    1947                                         continue;
    1948                                 }
    1949 
    1950                                 // HPOS-compatible: use order CRUD instead of get_post_meta.
    1951                                 $shipment_id = $order->get_meta( '_expressone_shipment_id', true );
    1952 
    1953                                 if ( ! $shipment_id ) {
    1954                                         $failed_orders[] = $order_id;
    1955                                         continue;
    1956                                 }
    1957 
    1958                                 $shipment_ids[]   = (int) $shipment_id;
    1959                                 $success_orders[] = $order_id;
    1960                         }
    1961 
    1962                         $result = [
    1963                                 'success' => $success_orders,
    1964                                 'failed'  => $failed_orders,
    1965                                 'pdf_url' => '',
    1966                                 'error'   => '',
    1967                         ];
    1968 
    1969                         if ( ! empty( $shipment_ids ) ) {
    1970                                 $api_key = express_one_get_api_key();
    1971 
    1972                                 if ( empty( $api_key ) ) {
    1973                                         $result['error'] = esc_html__( 'API Key is missing. Please configure it in Express One settings.', 'express-one-shipment' );
    1974                                 }
    1975 
    1976                                 $label_layout_id = $result['error'] ? '' : express_one_get_label_layout_id();
    1977                                 if ( ! $result['error'] && empty( $label_layout_id ) ) {
    1978                                         $result['error'] = esc_html__( 'Label Layout ID is missing. Please select a label layout in Express One settings.', 'express-one-shipment' );
    1979                                 }
    1980 
    1981                                 if ( ! $result['error'] ) {
    1982                                         $api_url = 'https://api.expressone.si/pdfinternal?layoutid=' . rawurlencode( $label_layout_id ) . '&apikey=' . rawurlencode( $api_key );
    1983                                         $body    = wp_json_encode( $shipment_ids );
    1984 
    1985                                         $response = wp_remote_post(
    1986                                                 $api_url,
    1987                                                 [
    1988                                                         'headers' => [ 'Content-Type' => 'application/json' ],
    1989                                                         'body'    => $body,
    1990                                                         'timeout' => 30,
    1991                                                 ]
    1992                                         );
    1993 
    1994                                         if ( is_wp_error( $response ) ) {
    1995                                                 $result['error'] = sprintf(
    1996                                                         /* translators: %s: underlying HTTP error message */
    1997                                                         esc_html__( 'Label service request failed: %s', 'express-one-shipment' ),
    1998                                                         esc_html( $response->get_error_message() )
    1999                                                 );
    2000                                         } else {
    2001                                                 $response_code = wp_remote_retrieve_response_code( $response );
    2002                                                 $response_body = wp_remote_retrieve_body( $response );
    2003                                                 $data          = json_decode( $response_body, true );
    2004 
    2005                                                 if ( $response_code < 200 || $response_code >= 300 ) {
    2006                                                         $api_error_msg    = ! empty( $data['error']['message'] ) ? (string) $data['error']['message'] : (string) $response_body;
    2007                                                         $result['error']  = sprintf(
    2008                                                                 /* translators: %s: error message returned by the label API */
    2009                                                                 esc_html__( 'Label service returned an error: %s', 'express-one-shipment' ),
    2010                                                                 esc_html( $api_error_msg )
    2011                                                         );
    2012                                                 } elseif ( empty( $data['labelsbase64'] ) ) {
    2013                                                         $result['error'] = esc_html__( 'Label service did not return any labels for the selected orders.', 'express-one-shipment' );
    2014                                                 } else {
    2015                                                         $pdf_binary = base64_decode( (string) $data['labelsbase64'] );
    2016 
    2017                                                         if ( ! $pdf_binary ) {
    2018                                                                 $result['error'] = esc_html__( 'Received label data could not be decoded.', 'express-one-shipment' );
    2019                                                         } else {
    2020                                                                 $upload_dir = wp_upload_dir();
    2021                                                                 $file_name  = 'shipment-labels-' . time() . '-' . wp_rand( 1000, 9999 ) . '.pdf';
    2022                                                                 $file_path  = trailingslashit( $upload_dir['basedir'] ) . $file_name;
    2023 
    2024                                                                 $fs = express_one_get_filesystem();
    2025                                                                 if ( ! $fs || ! $fs->put_contents( $file_path, $pdf_binary, FS_CHMOD_FILE ) ) {
    2026                                                                         $result['error'] = esc_html__( 'Failed to save the generated label PDF to disk.', 'express-one-shipment' );
    2027                                                                 } else {
    2028                                                                         $result['pdf_url'] = admin_url(
    2029                                                                                 'admin-post.php?action=download_bulk_labels&file=' . rawurlencode( $file_name ) .
    2030                                                                                 '&_wpnonce=' . wp_create_nonce( 'download_bulk_labels' )
    2031                                                                         );
    2032                                                                 }
    2033                                                         }
    2034                                                 }
    2035                                         }
    2036                                 }
    2037                         }
    2038 
    2039                         set_transient( 'bulk_labels_result_' . get_current_user_id(), $result, 60 );
    2040                         return wp_get_referer();
    2041                 },
    2042                 10,
    2043                 3
    2044         );
    2045 
    2046         add_action(
    2047                 'admin_notices',
    2048                 function() {
    2049                         $user_id = get_current_user_id();
    2050                         $result  = get_transient( 'bulk_labels_result_' . $user_id );
    2051 
    2052                         if ( false === $result ) {
    2053                                 return;
    2054                         }
    2055 
    2056                         delete_transient( 'bulk_labels_result_' . $user_id );
    2057 
    2058                         if ( ! empty( $result['success'] ) ) {
    2059                                 $msg = esc_html__( 'Labels created for orders: ', 'express-one-shipment' ) . implode( ', ', array_map( 'intval', $result['success'] ) );
    2060                                 if ( ! empty( $result['pdf_url'] ) ) {
    2061                                         $msg .= ' — <a href="' . esc_url( $result['pdf_url'] ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Download PDF', 'express-one-shipment' ) . '</a>';
    2062                                 }
    2063                                 echo '<div class="notice notice-success"><p>' . wp_kses_post( $msg ) . '</p></div>';
    2064                         }
    2065 
    2066                         if ( ! empty( $result['failed'] ) ) {
    2067                                 echo '<div class="notice notice-error"><p>' .
    2068                                         esc_html__( 'Failed to create labels for orders — no shipment exists yet for: ', 'express-one-shipment' ) .
    2069                                         esc_html( implode( ', ', array_map( 'intval', $result['failed'] ) ) ) .
    2070                                         '</p></div>';
    2071                         }
    2072 
    2073                         if ( ! empty( $result['error'] ) ) {
    2074                                 echo '<div class="notice notice-error"><p>' . wp_kses_post( $result['error'] ) . '</p></div>';
    2075                         }
    2076                 }
    2077         );
    2078 
    2079         add_action(
    2080                 'admin_post_download_bulk_labels',
    2081                 function() {
    2082                         if ( ! current_user_can( 'manage_woocommerce' ) ) {
    2083                                 wp_die( esc_html__( 'Unauthorized access.', 'express-one-shipment' ) );
    2084                         }
    2085 
    2086                         $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
    2087                         if ( ! $nonce || ! wp_verify_nonce( $nonce, 'download_bulk_labels' ) ) {
    2088                                 wp_die( esc_html__( 'Security check failed.', 'express-one-shipment' ) );
    2089                         }
    2090 
    2091                         if ( empty( $_GET['file'] ) ) {
    2092                                 wp_die( esc_html__( 'No file specified.', 'express-one-shipment' ) );
    2093                         }
    2094 
    2095                         $file_name = sanitize_file_name( wp_unslash( $_GET['file'] ) );
    2096                         if ( $file_name !== wp_basename( $file_name ) ) {
    2097                                 wp_die( esc_html__( 'Invalid file name.', 'express-one-shipment' ) );
    2098                         }
    2099 
    2100                         $upload_dir = wp_upload_dir();
    2101                         $file_path  = trailingslashit( $upload_dir['basedir'] ) . $file_name;
    2102 
    2103                         $fs = express_one_get_filesystem();
    2104                         if ( ! $fs ) {
    2105                                 wp_die( esc_html__( 'Filesystem not available.', 'express-one-shipment' ) );
    2106                         }
    2107 
    2108                         if ( ! $fs->exists( $file_path ) ) {
    2109                                 wp_die( esc_html__( 'File not found.', 'express-one-shipment' ) );
    2110                         }
    2111 
    2112                         $pdf_data = $fs->get_contents( $file_path );
    2113                         if ( false === $pdf_data || '' === $pdf_data ) {
    2114                                 wp_die( esc_html__( 'Failed to read file.', 'express-one-shipment' ) );
    2115                         }
    2116 
    2117                         $safe_filename = sanitize_file_name( basename( $file_path ) );
    2118 
    2119                         header( 'Content-Description: File Transfer' );
    2120                         header( 'Content-Type: application/pdf' );
    2121                         header( 'Content-Disposition: attachment; filename="' . $safe_filename . '"' );
    2122                         header( 'Content-Length: ' . strlen( $pdf_data ) );
    2123 
    2124                         // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Binary PDF output.
    2125                         echo $pdf_data;
    2126 
    2127                         $fs->delete( $file_path );
    2128                         exit;
    2129                 }
    2130         );
    2131 
    2132         /**
    2133          * Admin notices — HPOS-compatible
    2134          */
    2135         add_action(
    2136                 'admin_notices',
    2137                 function () {
    2138                         $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
    2139                         if ( ! $screen ) {
    2140                                 return;
    2141                         }
    2142 
    2143                         // Support both legacy and HPOS order edit screens.
    2144                         $is_legacy = ( 'post' === $screen->base && isset( $screen->post_type ) && 'shop_order' === $screen->post_type );
    2145                         $is_hpos   = ( 'woocommerce_page_wc-orders' === $screen->id );
    2146 
    2147                         if ( ! $is_legacy && ! $is_hpos ) {
    2148                                 return;
    2149                         }
    2150 
    2151                         // Get current order ID from either URL format.
    2152                         $order_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
    2153                         if ( ! $order_id ) {
    2154                                 $order_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
    2155                         }
    2156                         if ( ! $order_id ) {
    2157                                 return;
    2158                         }
    2159 
    2160                         if ( get_transient( 'express_one_shipment_created_' . $order_id ) ) {
    2161                                 delete_transient( 'express_one_shipment_created_' . $order_id );
    2162                                 echo '<div class="notice notice-success is-dismissible"><p>' .
    2163                                         esc_html__( 'Express One created successfully!', 'express-one-shipment' ) .
    2164                                 '</p></div>';
    2165                                 return;
    2166                         }
    2167 
    2168                         if ( get_transient( 'express_one_shipment_updated_' . $order_id ) ) {
    2169                                 delete_transient( 'express_one_shipment_updated_' . $order_id );
    2170                                 echo '<div class="notice notice-success is-dismissible"><p>' .
    2171                                         esc_html__( 'Express One updated successfully!', 'express-one-shipment' ) .
    2172                                 '</p></div>';
    2173                                 return;
    2174                         }
    2175 
    2176                         $error = get_transient( 'express_one_shipment_error_' . $order_id );
    2177                         if ( $error ) {
    2178                                 delete_transient( 'express_one_shipment_error_' . $order_id );
    2179                                 echo '<div class="notice notice-error is-dismissible"><p>' .
    2180                                         esc_html__( 'Shipment error: ', 'express-one-shipment' ) .
    2181                                         esc_html( $error ) .
    2182                                 '</p></div>';
    2183                                 return;
    2184                         }
    2185                 }
    2186         );
    2187 }
    2188 
    2189 /**
    2190  * Shipping Methods: Flat/Home Delivery
    2191  */
    2192 if ( ! class_exists( 'expressOne_Flat_Shipping_Options' ) ) {
    2193 
    2194         function express_one_flat_shipping_methods_init() {
    2195 
    2196                 class expressOne_Flat_Shipping_Options extends WC_Shipping_Method {
    2197 
    2198                         public function __construct() {
    2199                                 $this->id                 = 'express_one_flat_shipping';
    2200                                 $this->method_title       = __( 'Home Delivery Shipment', 'express-one-shipment' );
    2201                                 $this->title              = __( 'Home Delivery Shipment', 'express-one-shipment' );
    2202                                 $this->method_description = __( 'Home Delivery Shipment Description', 'express-one-shipment' );
    2203 
    2204                                 add_action( 'woocommerce_update_options_shipping_' . $this->id, [ $this, 'process_admin_options' ] );
    2205                                 $this->init();
    2206                         }
    2207 
    2208                         public function init() {
    2209                                 $this->init_form_fields();
    2210                                 $this->init_settings();
    2211 
    2212                                 $this->flatEnabled      = $this->get_option( 'flatEnabled' );
    2213                                 $this->flatTitle        = __( 'Home Delivery Shipment', 'express-one-shipment' );
    2214                                 $this->flatType         = 'fixed';
    2215                                 $this->flatFee          = $this->get_option( 'price' );
    2216                                 $this->flatCodes        = $this->get_option( 'flatCodes' );
    2217                                 $this->flatAvailability = $this->get_option( 'flatAvailability' );
    2218                                 $this->flatCountries    = $this->get_option( 'flatCountries' );
    2219                                 $this->freeFlatShipment = $this->get_option( 'freeFlatShipment' );
    2220 
    2221                                 add_filter( 'woocommerce_shipping_methods', [ $this, 'add_express_one_flat_shipping_methods' ] );
    2222                                 if ( ! is_admin() ) {
    2223                                         add_action( 'woocommerce_cart_shipping_method_full_label', [ $this, 'negative_flat_label' ], 10, 2 );
    2224                                 }
    2225                         }
    2226 
    2227                         public function calculate_shipping( $package = [] ) {
    2228                                 $shipping_total = 0;
    2229                                 $cart_total     = WC()->cart->cart_contents_total;
    2230                                 $flatFee        = ( trim( (string) $this->flatFee ) === '' ) ? 0 : (float) $this->flatFee;
    2231 
    2232                                 if ( $cart_total >= (float) $this->freeFlatShipment ) {
    2233                                         $shipping_total = 0;
    2234                                 } else {
    2235                                         if ( 'fixed' === $this->flatType ) {
    2236                                                 $shipping_total = $flatFee;
    2237                                         }
    2238 
    2239                                         if ( 'percent' === $this->flatType ) {
    2240                                                 $shipping_total = $package['contents_cost'] * ( $flatFee / 100 );
    2241                                         }
    2242 
    2243                                         if ( 'product' === $this->flatType ) {
    2244                                                 foreach ( $package['contents'] as $item_id => $values ) {
    2245                                                         $_product = $values['data'];
    2246 
    2247                                                         if ( $values['quantity'] > 0 && $_product->needs_shipping() ) {
    2248                                                                 $shipping_total += $flatFee * $values['quantity'];
    2249                                                         }
    2250                                                 }
    2251                                         }
    2252                                 }
    2253 
    2254                                 $shipping_total_with_tax = $shipping_total;
    2255                                 $tax_total               = 0;
    2256                                 $label_suffix            = $this->flatTitle;
    2257                                 $taxes                   = [];
    2258 
    2259                                 if ( wc_tax_enabled() ) {
    2260                                         $tax_rates = WC_Tax::get_shipping_tax_rates();
    2261                                         $taxes     = WC_Tax::calc_shipping_tax( $shipping_total, $tax_rates );
    2262                                         $tax_total = array_sum( $taxes );
    2263                                         $shipping_total_with_tax += $tax_total;
    2264 
    2265                                         $suffix_template = get_option( 'woocommerce_price_display_suffix', '' );
    2266 
    2267                                         $price_excl_tax = wc_price( $shipping_total );
    2268                                         $price_incl_tax = wc_price( $shipping_total_with_tax );
    2269                                         $tax_amount     = wc_price( $tax_total );
    2270 
    2271                                         $search         = [ '{tax_amount}', '{price_including_tax}', '{price_excluding_tax}' ];
    2272                                         $replace        = [ $tax_amount, $price_incl_tax, $price_excl_tax ];
    2273                                         $dynamic_suffix = str_replace( $search, $replace, $suffix_template );
    2274 
    2275                                         if ( ! empty( $dynamic_suffix ) ) {
    2276                                                 $label_suffix .= ' (' . $dynamic_suffix . ')';
    2277                                         }
    2278                                 }
    2279 
    2280                                 $rate = [
    2281                                         'id'    => $this->id,
    2282                                         'label' => $label_suffix,
    2283                                         'cost'  => $shipping_total,
    2284                                         'taxes' => $taxes,
    2285                                 ];
    2286 
    2287                                 $this->add_rate( $rate );
    2288                         }
    2289 
    2290                         public function init_form_fields() {
    2291                                 $this->form_fields = [
    2292                                         'flatEnabled'      => [
    2293                                                 'title'   => esc_html__( 'Enable', 'express-one-shipment' ),
    2294                                                 'type'    => 'checkbox',
    2295                                                 'label'   => esc_html__( 'Enable Home Delivery', 'express-one-shipment' ),
    2296                                                 'default' => 'no',
    2297                                         ],
    2298                                         'flatCodes'        => [
    2299                                                 'title'       => esc_html__( 'Zip/Post Codes', 'express-one-shipment' ),
    2300                                                 'type'        => 'textarea',
    2301                                                 'description' => esc_html__( 'What zip/post codes can use the local pickup option? Separate codes with a comma. Accepts wildcards, e.g. P* will match a postcode of PE30.', 'express-one-shipment' ),
    2302                                                 'default'     => '',
    2303                                                 'desc_tip'    => true,
    2304                                                 'placeholder' => '12345, 56789, etc',
    2305                                         ],
    2306                                         'price'            => [
    2307                                                 'title'       => esc_html__( 'Home Delivery Fee/Discount', 'express-one-shipment' ),
    2308                                                 'type'        => 'price',
    2309                                                 'description' => esc_html__( 'What fee do you want to charge for local pickup? Enter negative value for discount. Leave blank to disable.', 'express-one-shipment' ),
    2310                                                 'default'     => '',
    2311                                                 'desc_tip'    => true,
    2312                                                 'placeholder' => '0',
    2313                                         ],
    2314                                         'flatAvailability' => [
    2315                                                 'title'   => esc_html__( 'Method availability', 'express-one-shipment' ),
    2316                                                 'type'    => 'select',
    2317                                                 'default' => 'all',
    2318                                                 'class'   => 'availability',
    2319                                                 'options' => [
    2320                                                         'all'      => esc_html__( 'All allowed countries', 'express-one-shipment' ),
    2321                                                         'specific' => esc_html__( 'Specific Countries', 'express-one-shipment' ),
    2322                                                 ],
    2323                                         ],
    2324                                         'flatCountries'    => [
    2325                                                 'title'             => esc_html__( 'Specific Countries', 'express-one-shipment' ),
    2326                                                 'type'              => 'multiselect',
    2327                                                 'class'             => 'chosen_select',
    2328                                                 'css'               => 'width: 450px;',
    2329                                                 'default'           => '',
    2330                                                 'options'           => function_exists( 'WC' ) && WC()->countries ? WC()->countries->get_shipping_countries() : [],
    2331                                                 'custom_attributes' => [
    2332                                                         'data-placeholder' => esc_html__( 'Select some countries', 'express-one-shipment' ),
    2333                                                 ],
    2334                                         ],
    2335                                         'freeFlatShipment' => [
    2336                                                 'title'       => esc_html__( 'Custom Free Shipment', 'express-one-shipment' ),
    2337                                                 'type'        => 'price',
    2338                                                 'description' => esc_html__( 'Enter the users price. For example if it is greater than $50, shipping will be free.', 'express-one-shipment' ),
    2339                                                 'default'     => '',
    2340                                                 'desc_tip'    => true,
    2341                                         ],
    2342                                 ];
    2343                         }
    2344 
    2345                         public function admin_options() {
    2346                                 ?>
    2347                                 <h3><?php echo esc_html( $this->method_title ); ?></h3>
    2348                                 <p><?php esc_html_e( 'Home Delivery Shipment Description', 'express-one-shipment' ); ?></p>
    2349                                 <table class="form-table">
    2350                                         <?php $this->generate_settings_html(); ?>
    2351                                 </table>
    2352                                 <?php
    2353                         }
    2354 
    2355                         public function is_available( $package ) {
    2356                                 $flat_enabled = ( 'no' === $this->flatEnabled );
    2357                                 if ( $flat_enabled ) {
    2358                                         return false;
    2359                                 }
    2360 
    2361                                 $flatCodes = [];
    2362                                 if ( ! empty( $this->flatCodes ) ) {
    2363                                         foreach ( explode( ',', $this->flatCodes ) as $code ) {
    2364                                                 $flatCodes[] = $this->clean( $code );
    2365                                         }
    2366                                 }
    2367 
    2368                                 if ( is_array( $flatCodes ) && ! empty( $flatCodes ) ) {
    2369                                         $found_match = false;
    2370 
    2371                                         if ( in_array( $this->clean( $package['destination']['postcode'] ), $flatCodes, true ) ) {
    2372                                                 $found_match = true;
    2373                                         }
    2374 
    2375                                         if ( ! $found_match ) {
    2376                                                 $customer_postcode = $this->clean( $package['destination']['postcode'] );
    2377                                                 foreach ( $flatCodes as $c ) {
    2378                                                         $pattern = '/^' . str_replace( '_', '[0-9a-zA-Z]', preg_quote( $c, '/' ) ) . '$/i';
    2379                                                         if ( preg_match( $pattern, $customer_postcode ) ) {
    2380                                                                 $found_match = true;
    2381                                                                 break;
    2382                                                         }
    2383                                                 }
    2384                                         }
    2385 
    2386                                         if ( ! $found_match ) {
    2387                                                 $customer_postcode        = $this->clean( $package['destination']['postcode'] );
    2388                                                 $customer_postcode_length = strlen( $customer_postcode );
    2389 
    2390                                                 for ( $i = 0; $i <= $customer_postcode_length; $i++ ) {
    2391                                                         if ( in_array( $customer_postcode, $flatCodes, true ) ) {
    2392                                                                 $found_match = true;
    2393                                                         }
    2394                                                         $customer_postcode = substr( $customer_postcode, 0, -2 ) . '*';
    2395                                                 }
    2396                                         }
    2397 
    2398                                         if ( ! $found_match ) {
    2399                                                 return false;
    2400                                         }
    2401                                 }
    2402 
    2403                                 if ( 'specific' === $this->flatAvailability ) {
    2404                                         $ship_to_countries = $this->flatCountries;
    2405                                 } else {
    2406                                         $ship_to_countries = function_exists( 'WC' ) && WC()->countries ? array_keys( WC()->countries->get_shipping_countries() ) : [];
    2407                                 }
    2408 
    2409                                 if ( is_array( $ship_to_countries ) && ! empty( $ship_to_countries ) ) {
    2410                                         if ( ! in_array( $package['destination']['country'], $ship_to_countries, true ) ) {
    2411                                                 return false;
    2412                                         }
    2413                                 }
    2414 
    2415                                 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
    2416                                 return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', true, $package );
    2417                         }
    2418 
    2419                         public function clean( $code ) {
    2420                                 return str_replace( '-', '', sanitize_title( $code ) ) . ( strstr( $code, '*' ) ? '*' : '' );
    2421                         }
    2422 
    2423                         public function add_express_one_flat_shipping_methods( $methods ) {
    2424                                 $methods[] = $this;
    2425                                 return $methods;
    2426                         }
    2427 
    2428                         public function negative_flat_label( $label, $method ) {
    2429                                 if ( ! is_admin() && 'express_one_flat_shipping' === $method->id ) {
    2430                                         $label = $method->label . ' : ' . wc_price( $method->cost );
    2431                                 }
    2432                                 return $label;
    2433                         }
    2434                 }
    2435 
    2436                 new expressOne_Flat_Shipping_Options();
    2437         }
    2438 
    2439         add_action( 'woocommerce_shipping_init', 'express_one_flat_shipping_methods_init' );
    2440 }
     48// "Express One" admin menu and settings pages.
     49require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/admin-settings.php';
  • express-one-shipment/trunk/readme.txt

    r3636558 r3647198  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.0.7
     7Stable tag: 1.1.0
    88License: GPLv3
    99License URI: https://www.gnu.org/licenses/gpl-3.0.html
    … …  
    3636✔ COD (Cash on Delivery) support 
    3737✔ Bulk label printing 
    38 ✔ Full WooCommerce integration 
    39 ✔ Admin settings panels for both shipment types 
     38✔ Full WooCommerce integration
     39✔ Admin settings panels for both shipment types
     40✔ Optional weight-based dynamic pricing tiers for Pickup and Home Delivery, grouped per destination country
     41✔ Optional maximum parcel size limit for weight-based Pickup
    4042
    4143---
    … …  
    172174* Changed the "cargo" flag to false when creating a new shipment
    173175
     176= 1.1.0 =
     177* Added "Pickup Shipment (Weight-Based)" and "Home Delivery Shipment (Weight-Based)" shipping methods
     178* Added dynamic price tiers by parcel weight, grouped per destination country with a "Default" fallback group
     179* Added optional maximum parcel size (length + width + height) limit for the weight-based Pickup method
     180* Added mutual exclusivity between each fixed-rate method and its weight-based counterpart
     181* The new methods reuse the existing pickup-point map and Home Delivery courier API eligibility checks
     182* Internal refactor of the plugin's file structure — no functional changes to the existing fixed-rate methods
     183
    174184---
    175185
    … …  
    196206= 1.0.7 =
    197207Changes the "cargo" flag to false on shipment creation.
     208
     209= 1.1.0 =
     210Adds two new weight-based shipping methods (Pickup and Home Delivery) with dynamic, per-country price tiers. Existing Pickup Shipment and Home Delivery Shipment methods and their settings are unchanged — this is safe to upgrade with no action needed.
    198211
    199212---
Note: See TracChangeset for help on using the changeset viewer.