Changeset 3647198
- Timestamp:
- 08/14/2026 11:07:46 AM (2 months ago)
- Location:
- express-one-shipment
- Files:
-
- 51 added
- 4 edited
-
tags/1.1.0 (added)
-
tags/1.1.0/assets (added)
-
tags/1.1.0/assets/images (added)
-
tags/1.1.0/assets/images/marker-icon-blue.png (added)
-
tags/1.1.0/assets/images/marker-icon-red.png (added)
-
tags/1.1.0/assets/images/marker-shadow.png (added)
-
tags/1.1.0/assets/js (added)
-
tags/1.1.0/assets/js/admin-collie-count.js (added)
-
tags/1.1.0/assets/js/admin-main-settings.js (added)
-
tags/1.1.0/assets/js/admin-weight-tiers.js (added)
-
tags/1.1.0/assets/js/checkout-map.js (added)
-
tags/1.1.0/assets/vendor (added)
-
tags/1.1.0/assets/vendor/leaflet (added)
-
tags/1.1.0/assets/vendor/leaflet/leaflet-control-geocoder.css (added)
-
tags/1.1.0/assets/vendor/leaflet/leaflet-control-geocoder.js (added)
-
tags/1.1.0/assets/vendor/leaflet/leaflet.css (added)
-
tags/1.1.0/assets/vendor/leaflet/leaflet.js (added)
-
tags/1.1.0/changelog.txt (added)
-
tags/1.1.0/express-one-shipment.php (added)
-
tags/1.1.0/includes (added)
-
tags/1.1.0/includes/admin-settings.php (added)
-
tags/1.1.0/includes/class-express-one-flat-shipping.php (added)
-
tags/1.1.0/includes/class-express-one-flat-weight-shipping.php (added)
-
tags/1.1.0/includes/class-express-one-pickup-shipping.php (added)
-
tags/1.1.0/includes/class-express-one-pickup-weight-shipping.php (added)
-
tags/1.1.0/includes/order-integration.php (added)
-
tags/1.1.0/includes/pickup-map.php (added)
-
tags/1.1.0/includes/shipping-helpers.php (added)
-
tags/1.1.0/includes/traits (added)
-
tags/1.1.0/includes/traits/trait-country-weight-tiers-field.php (added)
-
tags/1.1.0/includes/traits/trait-shipping-common-fields.php (added)
-
tags/1.1.0/languages (added)
-
tags/1.1.0/languages/express-one-shipment-en_US.mo (added)
-
tags/1.1.0/languages/express-one-shipment-en_US.po (added)
-
tags/1.1.0/languages/express-one-shipment-si_SI.mo (added)
-
tags/1.1.0/languages/express-one-shipment-si_SI.po (added)
-
tags/1.1.0/languages/express-one-shipment.pot (added)
-
tags/1.1.0/readme.txt (added)
-
trunk/assets/js/admin-weight-tiers.js (added)
-
trunk/assets/js/checkout-map.js (modified) (1 diff)
-
trunk/changelog.txt (modified) (1 diff)
-
trunk/express-one-shipment.php (modified) (3 diffs)
-
trunk/includes (added)
-
trunk/includes/admin-settings.php (added)
-
trunk/includes/class-express-one-flat-shipping.php (added)
-
trunk/includes/class-express-one-flat-weight-shipping.php (added)
-
trunk/includes/class-express-one-pickup-shipping.php (added)
-
trunk/includes/class-express-one-pickup-weight-shipping.php (added)
-
trunk/includes/order-integration.php (added)
-
trunk/includes/pickup-map.php (added)
-
trunk/includes/shipping-helpers.php (added)
-
trunk/includes/traits (added)
-
trunk/includes/traits/trait-country-weight-tiers-field.php (added)
-
trunk/includes/traits/trait-shipping-common-fields.php (added)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
express-one-shipment/trunk/assets/js/checkout-map.js
r3494529 r3647198 113 113 function isPickupShippingMethod(methodValue) { 114 114 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; 118 121 } 119 122 -
express-one-shipment/trunk/changelog.txt
r3636558 r3647198 43 43 2026-08-06 - version 1.0.7 44 44 * 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 46 2026-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 5 5 Text Domain: express-one-shipment 6 6 Domain Path: /languages 7 Version: 1. 0.77 Version: 1.1.0 8 8 Author: Express One 9 9 Author URI: https://expressone.si/ … … 16 16 17 17 if ( ! defined( 'EXPRESS_ONE_SHIPMENT_VERSION' ) ) { 18 define( 'EXPRESS_ONE_SHIPMENT_VERSION', '1. 0.7' );18 define( 'EXPRESS_ONE_SHIPMENT_VERSION', '1.1.0' ); 19 19 } 20 20 … … 27 27 } 28 28 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. 30 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/shipping-helpers.php'; 31 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/traits/trait-shipping-common-fields.php'; 32 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/traits/trait-country-weight-tiers-field.php'; 36 33 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. 36 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/pickup-map.php'; 40 37 41 require_once ABSPATH . 'wp-admin/includes/file.php'; 42 $initialized = WP_Filesystem(); 38 // The four WooCommerce shipping method classes. 39 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/class-express-one-pickup-shipping.php'; 40 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/class-express-one-flat-shipping.php'; 41 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/class-express-one-pickup-weight-shipping.php'; 42 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/class-express-one-flat-weight-shipping.php'; 43 43 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. 46 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/order-integration.php'; 47 47 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§ion=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§ion=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. 49 require_once EXPRESS_ONE_SHIPMENT_PATH . 'includes/admin-settings.php'; -
express-one-shipment/trunk/readme.txt
r3636558 r3647198 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 1. 0.77 Stable tag: 1.1.0 8 8 License: GPLv3 9 9 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 36 36 ✔ COD (Cash on Delivery) support 37 37 ✔ 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 40 42 41 43 --- … … 172 174 * Changed the "cargo" flag to false when creating a new shipment 173 175 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 174 184 --- 175 185 … … 196 206 = 1.0.7 = 197 207 Changes the "cargo" flag to false on shipment creation. 208 209 = 1.1.0 = 210 Adds 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. 198 211 199 212 ---
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)