Changeset 3452263
- Timestamp:
- 02/02/2026 04:36:50 PM (8 weeks ago)
- Location:
- live-rates-for-shipstation
- Files:
-
- 26 added
- 10 deleted
- 26 edited
- 1 copied
-
tags/1.2.0 (copied) (copied from live-rates-for-shipstation/trunk)
-
tags/1.2.0/README.md (modified) (1 diff)
-
tags/1.2.0/_autoload.php (modified) (1 diff)
-
tags/1.2.0/_stallation.php (modified) (1 diff)
-
tags/1.2.0/changelog.txt (modified) (13 diffs)
-
tags/1.2.0/core/admin-edit-order.php (added)
-
tags/1.2.0/core/api/shipstation.php (modified) (17 diffs)
-
tags/1.2.0/core/api/shipstationv1.php (modified) (4 diffs)
-
tags/1.2.0/core/assets/js/admin.js (modified) (1 diff)
-
tags/1.2.0/core/assets/js/edit-order (added)
-
tags/1.2.0/core/assets/js/edit-order/_main.js (added)
-
tags/1.2.0/core/assets/js/shipping-zones/_main.js (modified) (1 diff)
-
tags/1.2.0/core/assets/views/customboxes-table.php (deleted)
-
tags/1.2.0/core/assets/views/edit-order (added)
-
tags/1.2.0/core/assets/views/edit-order/metabox-label-management.php (added)
-
tags/1.2.0/core/assets/views/services-table.php (deleted)
-
tags/1.2.0/core/assets/views/shipping-zone (added)
-
tags/1.2.0/core/assets/views/shipping-zone/customboxes-table.php (added)
-
tags/1.2.0/core/assets/views/shipping-zone/services-table.php (added)
-
tags/1.2.0/core/classes (added)
-
tags/1.2.0/core/classes/shipping-calculator.php (added)
-
tags/1.2.0/core/settings-shipstation.php (modified) (6 diffs)
-
tags/1.2.0/core/shipping-method-shipstation.php (modified) (20 diffs)
-
tags/1.2.0/core/traits/logger.php (modified) (2 diffs)
-
tags/1.2.0/core/wc-box-packer/class-wc-boxpack-box.php (deleted)
-
tags/1.2.0/core/wc-box-packer/class-wc-boxpack-item.php (deleted)
-
tags/1.2.0/core/wc-box-packer/class-wc-boxpack.php (deleted)
-
tags/1.2.0/core/wc-box-packer/wc-boxpack-box.php (added)
-
tags/1.2.0/core/wc-box-packer/wc-boxpack-item.php (added)
-
tags/1.2.0/core/wc-box-packer/wc-boxpack.php (added)
-
tags/1.2.0/live-rates-for-shipstation.php (modified) (4 diffs)
-
tags/1.2.0/readme.txt (modified) (3 diffs)
-
trunk/README.md (modified) (1 diff)
-
trunk/_autoload.php (modified) (1 diff)
-
trunk/_stallation.php (modified) (1 diff)
-
trunk/changelog.txt (modified) (13 diffs)
-
trunk/core/admin-edit-order.php (added)
-
trunk/core/api/shipstation.php (modified) (17 diffs)
-
trunk/core/api/shipstationv1.php (modified) (4 diffs)
-
trunk/core/assets/js/admin.js (modified) (1 diff)
-
trunk/core/assets/js/edit-order (added)
-
trunk/core/assets/js/edit-order/_main.js (added)
-
trunk/core/assets/js/shipping-zones/_main.js (modified) (1 diff)
-
trunk/core/assets/views/customboxes-table.php (deleted)
-
trunk/core/assets/views/edit-order (added)
-
trunk/core/assets/views/edit-order/metabox-label-management.php (added)
-
trunk/core/assets/views/services-table.php (deleted)
-
trunk/core/assets/views/shipping-zone (added)
-
trunk/core/assets/views/shipping-zone/customboxes-table.php (added)
-
trunk/core/assets/views/shipping-zone/services-table.php (added)
-
trunk/core/classes (added)
-
trunk/core/classes/shipping-calculator.php (added)
-
trunk/core/settings-shipstation.php (modified) (6 diffs)
-
trunk/core/shipping-method-shipstation.php (modified) (20 diffs)
-
trunk/core/traits/logger.php (modified) (2 diffs)
-
trunk/core/wc-box-packer/class-wc-boxpack-box.php (deleted)
-
trunk/core/wc-box-packer/class-wc-boxpack-item.php (deleted)
-
trunk/core/wc-box-packer/class-wc-boxpack.php (deleted)
-
trunk/core/wc-box-packer/wc-boxpack-box.php (added)
-
trunk/core/wc-box-packer/wc-boxpack-item.php (added)
-
trunk/core/wc-box-packer/wc-boxpack.php (added)
-
trunk/live-rates-for-shipstation.php (modified) (4 diffs)
-
trunk/readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
live-rates-for-shipstation/tags/1.2.0/README.md
r3407166 r3452263 42 42 43 43 This is a free plugin entirely volunteer run. While we will ensure that the plugin is up-to-date for any security issues or breakfixes, we cannot commit to supporting any customizations to the plugin or feature requests, but welcome these questions and requests through the [WordPress Support Forums](https://wordpress.org/support/plugin/live-rates-for-shipstation/) for our team to consider in a future release of the plugin. We recommend using the WordPress Forums for all inquiries. 44 45 ## Filter Hooks 46 47 - `iqlrss/cache/cart_rates` 48 - `iqlrss/cache/shipstation` 49 - `iqlrss/cache/shipstation_expires` 50 - `iqlrss/shipping/calculator_object` 51 - `iqlrss/shipping/packages` 52 - `iqlrss/zone/package_presets` 53 - `iqlrss/zone/settings` -
live-rates-for-shipstation/tags/1.2.0/_autoload.php
r3442676 r3452263 21 21 ) ); 22 22 23 /* Vroom! */ 24 if( 'driver' === $class_path ) { 25 $file_path = wp_normalize_path( sprintf( '%s/%s', 26 rtrim( plugin_dir_path( __FILE__ ), '\\/' ), 27 'live-rates-for-shipstation.php' 28 ) ); 29 } 30 23 31 if( file_exists( $file_path ) ) { 24 32 require_once $file_path; -
live-rates-for-shipstation/tags/1.2.0/_stallation.php
r3407166 r3452263 20 20 21 21 /** 22 * Unin tsall Plugin22 * Uninstall Plugin 23 23 */ 24 24 public static function uninstall() { -
live-rates-for-shipstation/tags/1.2.0/changelog.txt
r3442676 r3452263 2 2 3 3 This is a brief text document keeping track of changes to the plugin. For a full history, see the Github Repository. 4 5 = 1.2.0 = 6 7 Relase Date: Eventually. 8 9 * Overview 10 * Shipping Calculations moved to it's own Class for better PHPUnit testing. 11 * See unit-tests branch; work-in-progress. 12 * ShipStation Warehouses are now supported Globally and per Shipping Zone. 13 * Defaults to WooCommerce Store. Global overrides Store. Zone overrides Global. 14 * ShipStation Packages are now integrated into Custom Packages. 15 * Multiple new filter hooks added to manage caching, warehouses, and Shipping Calculator object. 16 17 * Code Updates 18 * Filter Hook `iqlrss/cache/shipstation` 19 * Boolean to disable caching for API requests. 20 * Filter Hook `iqlrss/cache/shipstation_expires` 21 * Integer seconds for how long to cache package rates for a customer. 22 * 1 Week is the default. 23 * Filter Hook `iqlrss/cache/cart_rates` 24 * Boolean to diable caching at a cart level. 25 * This is not recommended to disable since WC (sometimes) makes multiple calls to the cart per page load, but the option is available if you have a usecase. 26 * Fitler Hook `iqlrss/shipping/calculator_object` 27 * Expects a \IQLRSS\Core\Classes\Shipping_Calculator Object. 28 * Must inherit the IQLRSS Calculator to be valid. 29 * The shipping method calculations had an overhaul and broken into multiple methods of the new Shipping Calculator object. 30 * This is for PHPUnit testing and to make it easier for developers to override specific calculator functionality for their usecases. 4 31 5 32 = 1.1.2 = … … 15 42 = 1.1.1 = 16 43 17 Rel ase Date: December 04, 202544 Release Date: December 04, 2025 18 45 19 46 * Overview … … 23 50 = 1.1.0 = 24 51 25 Rel ase Date: December 01, 202552 Release Date: December 01, 2025 26 53 27 54 * Overview … … 58 85 = 1.0.8 = 59 86 60 Rel ase Date: October 10, 202587 Release Date: October 10, 2025 61 88 62 89 * Overview … … 74 101 * Ugh, the Block Editor + WooCommerce makes _multiple_ async requests to shipping calculations which would re-trigger things unnecessarily. 75 102 * The new methodology uses the WC()->session to automatically return the known rates if the cart has not changed. 76 * This is a notic able increase in speed when browsing the shop when your cart hasn't necessarily changed.77 * Caching layer also prevents multiple / dupl ciate API requests being logged which is nice.103 * This is a noticeable increase in speed when browsing the shop when your cart hasn't necessarily changed. 104 * Caching layer also prevents multiple / duplicate API requests being logged which is nice. 78 105 79 106 * Code Updates … … 83 110 = 1.0.7 = 84 111 85 Rel ase Date: October 08, 2025112 Release Date: October 08, 2025 86 113 87 114 * Overview … … 89 116 * This denotes what items got what rates, the adjustments, where the adjustments come from. 90 117 * Patches issue on Shipping Zone where WP_Error was treated as an Exception 91 * Starts to nor amlize code for carrier_id and carrier_code.118 * Starts to normalize code for carrier_id and carrier_code. 92 119 * Doing this makes it easier to integrate the v1 API. 93 120 * The Carrier ID is the ShipStation `se-` code. … … 102 129 = 1.0.6 = 103 130 104 Rel ase Date: September 22, 2025131 Release Date: September 22, 2025 105 132 106 133 * Overview … … 109 136 = 1.0.5 = 110 137 111 Rel ase Date: September 16, 2025138 Release Date: September 16, 2025 112 139 113 140 * Overview … … 135 162 = 1.0.4 = 136 163 137 Rel ase Date: September 15, 2025164 Release Date: September 15, 2025 138 165 139 166 * Overview … … 153 180 = 1.0.3 = 154 181 155 Rel ase Date: August 05, 2025182 Release Date: August 05, 2025 156 183 157 184 * Overview … … 165 192 = 1.0.2 = 166 193 167 Rel ase Date: August 04, 2025194 Release Date: August 04, 2025 168 195 169 196 * Overview … … 177 204 = 1.0.1 = 178 205 179 Rel ase Date: August 01, 2025206 Release Date: August 01, 2025 180 207 181 208 * Overview -
live-rates-for-shipstation/tags/1.2.0/core/api/shipstation.php
r3407166 r3452263 2 2 /** 3 3 * ShipStation API Helper 4 * 5 * @link https://docs.shipstation.com/openapi 4 6 * 5 7 * Carrier ID : se-* … … 10 12 */ 11 13 namespace IQLRSS\Core\Api; 14 use \IQLRSS\Core\Traits; 12 15 13 16 if( ! defined( 'ABSPATH' ) ) { … … 18 21 19 22 /** 23 * Inherit logger traits 24 */ 25 use Traits\Logger; 26 27 28 /** 20 29 * Skip cache check 21 30 * … … 67 76 $this->prefix = \IQLRSS\Driver::get( 'slug' ); 68 77 $this->key = \IQLRSS\Driver::get_ss_opt( 'api_key', '' ); 69 $this->skip_cache = (boolean)$skip_cache; 70 $this->cache_time = defined( 'WEEK_IN_SECONDS' ) ? WEEK_IN_SECONDS : 604800; 78 79 80 /** 81 * Skip caching for the API. 82 * 83 * @hook filter 84 * 85 * @param Bolean FALSE 86 * 87 * @return Boolean 88 */ 89 $this->skip_cache = (boolean)apply_filters( 'iqlrss/cache/shipstation', $skip_cache, $this ); 90 91 92 /** 93 * Allow filtering the cache time. 94 * 95 * @see https://codex.wordpress.org/Easier_Expression_of_Time_Constants 96 * 97 * @hook filter 98 * 99 * @param Integer $cache_time - Week in seconds. 100 * 101 * @return Boolean 102 */ 103 $cache_time = apply_filters( 'iqlrss/cache/shipstation_expires', $this->cache_time, $this ); 104 $this->cache_time = ( is_numeric( $cache_time ) ) ? absint( $cache_time ) : $this->cache_time; 71 105 72 106 } … … 101 135 // Return Early - Something went wrong getting carriers. 102 136 } else if( ! isset( $carriers[ $carrier_code ] ) ) { 103 return $this->log( new \WP_Error( 404, esc_html__( 'Could not find carrier information.', 'live-rates-for-shipstation' ) ) );137 return $this->log( new \WP_Error( 404, esc_html__( 'Could not find carrier information.', 'live-rates-for-shipstation' ) ), 'warning' ); 104 138 } 105 139 … … 233 267 * @todo Look into `delivery_days` field. UPS has, is it carrier consistent? 234 268 * 235 * @param Array $est_opts 236 * 237 * @return Array|WP_Error 238 */ 239 public function get_shipping_estimates( $est_opts ) { 240 241 $body = $this->make_request( 'post', 'rates/estimate', $est_opts ); 269 * @link https://docs.shipstation.com/openapi/rates/calculate_rates 270 * @link https://docs.shipstation.com/openapi/rates/estimate_rates 271 * 272 * @param Array $api_args - See ShipStation API docs for required fields. 273 * 274 * @return Array|WP_Error 275 */ 276 public function get_shipping_estimates( $api_args ) { 277 278 $body = $this->make_request( 'post', 'rates/estimate', $api_args ); 242 279 243 280 // Return Early - API Request error - see logs. … … 284 321 285 322 /** 286 * Create a new Shipment 287 * 288 * @param Array $args 289 * 290 * @return Array $data 291 */ 292 public function create_shipments( $args ) { 293 294 $body = $this->make_request( 'post', 'shipments', array( 'shipments' => $args ) ); 323 * Return a single warehouse as a flat array of key value pairs. 324 * 325 * @param String $warehouse_id - Shipstation specific reference code. 326 * 327 * @return Array|WP_Error 328 */ 329 public function get_warehouse( $warehouse_id ) { 330 331 $warehouses = $this->get_warehouses(); 332 if( is_wp_error( $warehouses ) || empty( $warehouses ) ) { 333 return $warehouses; 334 } 335 336 return ( isset( $warehouses[ $warehouse_id ] ) ) ? $warehouses[ $warehouse_id ] : array(); 337 338 } 339 340 341 /** 342 * Return an array of Warehouses. 343 * 344 * @link https://docs.shipstation.com/openapi/warehouses/list_warehouses 345 * 346 * @return Array|WP_Error 347 */ 348 public function get_warehouses() { 349 350 $trans_key = $this->prefix_key( 'warehouses' ); 351 $warehouses = get_transient( $trans_key ); 352 353 if( empty( $warehouses ) || $this->skip_cache ) { 354 355 $body = $this->make_request( 'get', 'warehouses' ); 356 357 // Return Early - API Request error - see logs. 358 if( is_wp_error( $body ) ) { 359 return $body; 360 } 361 362 // Return Early - No Warehouses to work with. 363 if( empty( $body['warehouses'] ) ) { 364 return array(); 365 } 366 367 // We do need most the Warehouse data, but not all. 368 $warehouses = array(); 369 foreach( $body['warehouses'] as $warehouse_data ) { 370 371 $warehouse = array_intersect_key( $warehouse_data, array_flip( array( 372 'warehouse_id', 373 'is_default', 374 'name', 375 'origin_address', 376 'return_address', 377 ) ) ); 378 379 if( $warehouse['is_default'] ) { 380 $warehouse['name'] .= ' (' . esc_html__( 'ShipStation Default', 'live-rates-for-shipstation' ) . ')'; 381 } 382 383 $warehouses[ $warehouse['warehouse_id'] ] = $warehouse; 384 385 } 386 387 // Cache Warehouse data. 388 if( ! empty( $warehouses ) ) { 389 set_transient( $trans_key, $warehouses, $this->cache_time ); 390 } 391 } 392 393 return $warehouses; 394 395 } 396 397 398 /** 399 * Return a single package by ID. 400 * 401 * @param String $package_id - Shipstation specific reference code. 402 * 403 * @return Array|WP_Error 404 */ 405 public function get_package( $package_id ) { 406 407 $packages = $this->get_packages(); 408 if( is_wp_error( $packages ) || empty( $packages ) ) { 409 return $packages; 410 } 411 412 return ( isset( $packages[ $package_id ] ) ) ? $packages[ $package_id ] : array(); 413 414 } 415 416 417 /** 418 * Return an array of Packages 419 * 420 * @link https://docs.shipstation.com/openapi/package_types/list_package_types 421 * 422 * @return Array|WP_Error 423 */ 424 public function get_packages() { 425 426 $trans_key = $this->prefix_key( 'packages' ); 427 $packages = get_transient( $trans_key ); 428 429 if( empty( $packages ) || $this->skip_cache ) { 430 431 $body = $this->make_request( 'get', 'packages' ); 432 433 // Return Early - API Request error - see logs. 434 if( is_wp_error( $body ) ) { 435 return $body; 436 } 437 438 // Return Early - No Custom Packages to work with. 439 if( empty( $body['packages'] ) ) { 440 return array(); 441 } 442 443 // We do need most the Package data, just id, name, dimensions - ezpz. 444 $packages = array(); 445 foreach( $body['packages'] as $package_data ) { 446 447 $package = array_intersect_key( $package_data, array_flip( array( 448 'package_id', 449 'name', 450 'dimensions', 451 ) ) ); 452 453 $packages[ $package['package_id'] ] = $package; 454 455 } 456 457 // Cache Warehouse data. 458 if( ! empty( $packages ) ) { 459 set_transient( $trans_key, $packages, $this->cache_time ); 460 } 461 } 462 463 return $packages; 464 465 } 466 467 468 /** 469 * Purchase a shipping label by a carrier. 470 * 471 * @link https://docs.shipstation.com/openapi/labels/create_label 472 * 473 * @param Array $api_args - See ShipStation API docs for required fields. 474 * @param Boolean $test_label - Whether or not to create a test label. 475 * 476 * @return Array|WP_Error 477 */ 478 public function purchase_shipping_label( $api_args, $test_label = false ) { 479 480 if( $test_label ) $api_args['test_label'] = true; 481 $body = $this->make_request( 'post', 'labels', $api_args ); 295 482 296 483 // Return Early - API Request error - see logs. … … 299 486 } 300 487 301 /** 302 * API returns no errors but also doesn't do anything in ShipStation. 303 */ 304 $data = $body; 488 $data = array(); 305 489 306 490 return $data; … … 309 493 310 494 311 312 495 /** 313 496 * Create Shipments from given WC_Orders. … … 317 500 * @return Array|WP_Error 318 501 */ 319 public function create_shipments_from_wc_orders( $wc_orders ) {502 public function shipment_args_from_wc_orders( $wc_orders ) { 320 503 321 504 $data = array(); … … 325 508 326 509 $shipments = array(); 327 foreach( $wc_orders as $wc_order ) {510 foreach( (array)$wc_orders as $wc_order ) { 328 511 329 512 // Skip … … 337 520 $order_items = $wc_order->get_items(); 338 521 $order_item_ship = $wc_order->get_items( 'shipping' ); 339 $order_item_ship = ( ! empty( $order_item_ship ) ) ? $order_item_ship[ array_key_first( $order_item_ship ) ] : null; 340 522 $shipmentItem = ( ! empty( $order_item_ship ) ) ? $order_item_ship[ array_key_first( $order_item_ship ) ] : null; 523 524 // Return Early - No shipping item found. 525 if( null === $shipmentItem ) { 526 return new \WP_Error( 400, esc_html__( 'No shipping item found in order.', 'live-rates-for-shipstation' ) ); 527 } 528 529 // Shipment args I'll eventually need 530 $others = array( 531 'hold_until_date' => 'Y-m-d UTC', // ShipStation will hold the shipment until this date. Maybe leave blank, but could impact preorder items? 532 'ship_by_date' => 'Y-m-d UTC', // Purely informational - useful for store owners collecting additional data. 533 'ship_date' => 'Y-m-d UTC', // The date the shipment is actually shipped. Defaults to current date. 534 'requested_shipment_service' => 'carrier_service_code', // ex. ups_ground 535 'warehouse_id' => 'se-*', // ShipStation warehouse association: WC_Order Label Override > Shipping Zone > Global. We can skip ship_from if we have this. 536 'return_to' => array(), // Return to address info - need settings for this. 537 'advanced_options' => array(), // Advanced options - might need settings for this. 538 ); 539 540 // Default ShipStation Shipment Array 341 541 $shipment = array( 342 'validate_address' => 'no_validation', 343 'carrier_id' => $order_item_ship->get_meta( '_iqlrss_carrier_id', true ), 344 'store_id' => \IQLRSS\Driver::get_ss_opt( 'store_id' ), 542 'external_order_id' => $wc_order->get_id(), 543 'order_source_code'=> 'woocommerce', 544 'carrier_id' => $shipmentItem->get_meta( '_iqlrss_carrier_id', true ), 545 'service_code' => $shipmentItem->get_meta( '_iqlrss_service_code', true ), 345 546 'shipping_paid' => array( 346 547 'currency' => $wc_order->get_currency(), … … 377 578 ); 378 579 379 $shipment['items'] = array(); 580 // Add Packages 581 // $packages = $shipmentItem->get_meta( 'boxes', true ); 582 // if( ! empty( $packages ) && is_array( $packages ) ) { 583 // foreach( $packages as $package ) { 584 585 // $shipment['packages'][] = array( 586 // 'package_code' => $ship_package['packageCode'], 587 // 'package_name' => $ship_package['packageName'], 588 // 'weight' => array( 589 // 'value' => $ship_package['weight']['value'], 590 // 'unit' => $ship_package['weight']['unit'], 591 // ), 592 // 'dimensions' => array( 593 // 'length' => $ship_package['dimensions']['length'], 594 // 'width' => $ship_package['dimensions']['width'], 595 // 'height' => $ship_package['dimensions']['height'], 596 // 'unit' => $ship_package['dimensions']['unit'], 597 // ), 598 // ); 599 600 // } 601 // } 602 603 // Add Order Items 380 604 foreach( $shipstation_order_arr['items'] as $ship_item ) { 381 605 … … 434 658 } 435 659 436 return $this->create_shipments( $shipments );437 438 660 } 439 661 … … 506 728 // Return Early - API encountered an error. 507 729 if( is_wp_error( $request ) ) { 508 return $this->log( $request );730 return $this->log( $request, 'error' ); 509 731 } else if( 200 != $code || ! is_array( $body ) ) { 510 732 … … 524 746 } 525 747 526 return $this->log( new \WP_Error( $err_code, $err_msg ) );748 return $this->log( new \WP_Error( $err_code, $err_msg ), 'error' ); 527 749 } 528 750 529 751 // Log API Request Result 530 752 /* translators: %s is the API endpoint (example: carriers/rates). */ 531 $this->log( sprintf( esc_html__( 'ShipStation API Request to %s', 'live-rates-for-shipstation' ), $endpoint ), ' info', array(753 $this->log( sprintf( esc_html__( 'ShipStation API Request to %s', 'live-rates-for-shipstation' ), $endpoint ), 'debug', array( 532 754 'args' => $args, 533 755 'code' => $code, … … 575 797 } 576 798 577 578 /**579 * Log error in WooCommerce580 * Passthru method - log what's given and give it back.581 *582 * @param Mixed $error - String or WP_Error583 * @param String $level - WooCommerce level (debug|info|notice|warning|error|critical|alert|emergency)584 * @param Array $context585 *586 * @return Mixed - Return the error back.587 */588 protected function log( $error, $level = 'debug', $context = array() ) {589 590 if( ! \IQLRSS\Driver::get_ss_opt( 'logging_enabled', 0, true ) ) {591 return $error;592 }593 594 if( is_wp_error( $error ) ) {595 $error_msg = sprintf( '(%s) %s', $error->get_error_code(), $error->get_error_message() );596 } else {597 $error_msg = $error;598 }599 600 if( class_exists( '\WC_Logger' ) ) {601 602 if( null === $this->logger ) {603 $this->logger = \wc_get_logger();604 }605 606 /**607 * The WC_Logger does not handle double quotes well.608 * This will conver double quotes to faux: " -> ''609 */610 array_walk_recursive( $context, function( &$val ) {611 $val = ( is_string( $val ) ) ? str_replace( '"', "''", $val ) : $val;612 } );613 614 $this->logger->log( $level, $error_msg, array_merge( $context, array( 'source' => 'live-rates-for-shipstation' ) ) );615 616 }617 618 return $error;619 620 }621 622 799 } -
live-rates-for-shipstation/tags/1.2.0/core/api/shipstationv1.php
r3407166 r3452263 146 146 // Return Early - Something went wrong getting carriers. 147 147 } else if( ! isset( $carriers[ $carrier_code ] ) ) { 148 return $this->log( new \WP_Error( 404, esc_html__( '[v1] Could not find carrier information.', 'live-rates-for-shipstation' ) ) );148 return $this->log( new \WP_Error( 404, esc_html__( '[v1] Could not find carrier information.', 'live-rates-for-shipstation' ) ), 'warning' ); 149 149 } 150 150 … … 527 527 } ); 528 528 529 // Return Early - Skip the log but o529 // Return Early - No orders to work with. 530 530 if( empty( $orders ) ) { 531 return $this->log( new \WP_Error( 400, esc_html__( '[v1] Empty Orders. Data may be missing orderNumber or orderKey.', 'live-rates-for-shipstation' ) ), ' warning', array(531 return $this->log( new \WP_Error( 400, esc_html__( '[v1] Empty Orders. Data may be missing orderNumber or orderKey.', 'live-rates-for-shipstation' ) ), 'error', array( 532 532 'orders' => $order_arr, 533 533 ) ); … … 654 654 // Log API Request Result 655 655 /* translators: %s is the API endpoint (example: carriers/rates). */ 656 $this->log( sprintf( esc_html__( '[v1] ShipStation API Request to %s', 'live-rates-for-shipstation' ), $endpoint ), ' info', array(656 $this->log( sprintf( esc_html__( '[v1] ShipStation API Request to %s', 'live-rates-for-shipstation' ), $endpoint ), 'debug', array( 657 657 'args' => $args, 658 658 'code' => $code, 659 're ponse' => $body,659 'response' => $body, 660 660 ) ); 661 661 … … 692 692 */ 693 693 public function get_shipping_estimates( $est_opts ) { 694 return $this->log( new \WP_Error( 400, esc_html__( 'Live Rates for ShipStation v1 API Class does not support this endpoint. Use the v2 API Class: \IQLRSS\Core\Api\Shipstation', 'live-rates-for-shipstation' ) ) );694 return $this->log( new \WP_Error( 400, esc_html__( 'Live Rates for ShipStation v1 API Class does not support this endpoint. Use the v2 API Class: \IQLRSS\Core\Api\Shipstation', 'live-rates-for-shipstation' ) ), 'notice' ); 695 695 } 696 696 -
live-rates-for-shipstation/tags/1.2.0/core/assets/js/admin.js
r3407166 r3452263 19 19 } ); 20 20 } 21 22 23 /** 24 * Edit Order Module Settings 25 * @import editOrderSettings 26 */ 27 if( document.querySelector( 'form#order' ) ) { 28 import( './edit-order/_main.js' ).then( ( Module ) => { 29 new Module.editOrderSettings(); 30 } ); 31 } -
live-rates-for-shipstation/tags/1.2.0/core/assets/js/shipping-zones/_main.js
r3407166 r3452263 4 4 * Not really meant to be used as an object but more for 5 5 * encapsulation and organization. 6 * 7 * Manages the the possible initialization of custom boxes. 8 * Manages the show/hide functionality of price adjustments. 6 9 * 7 10 * @global {Object} iqlrss - Localized object of saved values. -
live-rates-for-shipstation/tags/1.2.0/core/settings-shipstation.php
r3411187 r3452263 167 167 public function enqueue_admin_assets() { 168 168 169 global $wp_scripts;170 171 169 if( ! $this->maybe_enqueue( 'admin' ) ) { 172 170 return; … … 175 173 wp_enqueue_style( \IQLRSS\Driver::plugin_prefix( 'admin', '-' ) ); 176 174 wp_enqueue_script_module( \IQLRSS\Driver::plugin_prefix( 'admin', '-' ) ); 177 178 // if( current_user_can( 'list_users' ) ) {179 // $foo = wp_script_modules();180 // printf( '<pre>%s</pre>', print_r( $foo, 1 ) );181 // die( 'end' );182 // }183 175 184 176 } … … 211 203 212 204 global $wpdb; 205 213 206 214 207 /** … … 297 290 '' => esc_html__( 'ShipStation carriers may still be loading...', 'live-rates-for-shipstation' ), 298 291 ); 292 $warehouses = array( 293 '' => '(' . esc_html__( 'Website Store Address', 'live-rates-for-shipstation' ) . ')', 294 ); 299 295 $appended_fields = array(); 300 296 301 297 if( ! empty( \IQLRSS\Driver::get_ss_opt( 'api_key' ) ) ) { 302 298 303 $carrier_desc = esc_html__( 'Select which ShipStation carriers you would like to see live shipping rates from.', 'live-rates-for-shipstation' ); 304 $response = ( new Api\Shipstation() )->get_carriers(); 305 299 $api = new Api\Shipstation(); 300 301 // Grab Warehouse options 302 $api_warehouses = $api->get_warehouses(); 303 if( is_a( $api_warehouses, 'WP_Error' ) ) { 304 $warehouses = array( '' => $api_warehouses->get_error_message() ); 305 } else if( is_array( $api_warehouses ) && ! empty( $api_warehouses ) ) { 306 $warehouses = array_merge( $warehouses, array_combine( 307 array_keys( $api_warehouses ), 308 array_column( $api_warehouses, 'name' ), 309 ) ); 310 } 311 312 // Grab Carrier options 306 313 $carriers = array(); 307 if( is_a( $response, 'WP_Error' ) ) { 308 $carriers[''] = $response->get_error_message(); 309 } else if( is_array( $response ) ) { 310 foreach( $response as $carrier ) { 314 $api_carriers = $api->get_carriers(); 315 if( is_a( $api_carriers, 'WP_Error' ) ) { 316 $carriers[''] = $api_carriers->get_error_message(); 317 } else if( is_array( $api_carriers ) && ! empty( $api_carriers ) ) { 318 foreach( $api_carriers as $carrier ) { 311 319 $carriers[ $carrier['carrier_id'] ] = $carrier['name']; 312 320 } 313 321 } 314 322 315 } else {316 $carrier_desc = esc_html__( 'Please set and verify your ShipStation API key. Then, click the Save button at the bottom of this page.', 'live-rates-for-shipstation' );317 323 } 318 324 … … 340 346 'class' => 'chosen_select', 341 347 'options' => $carriers, 342 'description' => $carrier_desc, 348 'description' => ( function() { 349 if( ! empty( \IQLRSS\Driver::get_ss_opt( 'api_key' ) ) ) { 350 return esc_html__( 'Select which ShipStation carriers you would like to see live shipping rates from.', 'live-rates-for-shipstation' ); 351 } 352 return esc_html__( 'Please set and verify your ShipStation API key. Then, click the Save button at the bottom of this page.', 'live-rates-for-shipstation' ); 353 } )(), 343 354 'desc_tip' => esc_html__( 'Services from selected carriers will be available when setting up Shipping Zones.', 'live-rates-for-shipstation' ), 355 'default' => '', 356 ); 357 358 $appended_fields[ \IQLRSS\Driver::plugin_prefix( 'global_warehouse' ) ] = array( 359 'title' => esc_html__( 'Shipping From', 'live-rates-for-shipstation' ), 360 'type' => 'select', 361 'options' => $warehouses, 362 'description' => ( function() { 363 if( ! empty( \IQLRSS\Driver::get_ss_opt( 'api_key' ) ) ) { 364 return esc_html__( 'Select to ship from a different location than what is set as your WooCommerce website default location.', 'live-rates-for-shipstation' ); 365 } 366 return esc_html__( 'Please set and verify your ShipStation API key. Then, click the Save button at the bottom of this page.', 'live-rates-for-shipstation' ); 367 } )(), 368 'desc_tip' => esc_html__( 'This can be overridden per Shipping Zone.', 'live-rates-for-shipstation' ), 344 369 'default' => '', 345 370 ); … … 503 528 $enqueue = ( $enqueue || ( isset( $_GET, $_GET['section'] ) && 'shipstation' == $_GET['section'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 504 529 530 // Edit Order page 531 $enqueue = ( $enqueue || ( isset( $_GET, $_GET['page'], $_GET['id'] ) && 'wc-orders' == $_GET['page'] && ! empty( $_GET['id'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 532 505 533 // Overprotective WooCommerce settings page check 506 $enqueue = ( $enqueue && 'woocommerce_page_wc-settings' == $screen_id);534 $enqueue = ( $enqueue && in_array( $screen_id, array( 'woocommerce_page_wc-orders', 'woocommerce_page_wc-settings' ) ) ); 507 535 } 508 536 return $enqueue; -
live-rates-for-shipstation/tags/1.2.0/core/shipping-method-shipstation.php
r3442676 r3452263 2 2 /** 3 3 * ShipStation Live Shipping Rates Method 4 *5 * @todo Consider moving Shipping Calculations into it's own class.6 4 * 7 5 * @link https://www.fedex.com/en-us/shipping/one-rate.html … … 16 14 */ 17 15 namespace IQLRSS\Core; 16 use \IQLRSS\Core\Traits; 18 17 19 18 if( ! defined( 'ABSPATH' ) ) { … … 26 25 27 26 /** 27 * Inherit logger traits 28 */ 29 use Traits\Logger; 30 31 32 /** 28 33 * Plugin prefix used to namespace data keys. 29 34 * … … 31 36 */ 32 37 protected $plugin_prefix; 33 34 35 /**36 * Array of store specific settings.37 *38 * @var Array39 */40 protected $store_data = array(41 'weight_unit' => '',42 'dim_unit' => '', // Dimension43 );44 38 45 39 … … 94 88 } 95 89 96 // Set the store unit term and associate it with ShipStations term.97 $this->store_data = array(98 'weight_unit' => get_option( 'woocommerce_weight_unit', $this->store_data['weight_unit'] ),99 'dim_unit' => get_option( 'woocommerce_dimension_unit', $this->store_data['dim_unit'] ),100 );101 102 90 /** 103 91 * Init shipping methods. … … 189 177 /** 190 178 * Increase the HTTP Request Timeout 191 * Sometimes ShipStation takes awhile to respond ewith rates.179 * Sometimes ShipStation takes awhile to respond with rates. 192 180 * Presumably, the more services enabled, the longer it takes. 193 181 * … … 409 397 protected function init_instance_form_fields() { 410 398 399 $store_warehouse_label = '(' . esc_html__( 'Website Store Address', 'live-rates-for-shipstation' ) . ')'; 400 $warehouses = array( '' => $store_warehouse_label ); 401 402 if( ! empty( \IQLRSS\Driver::get_ss_opt( 'api_key' ) ) ) { 403 404 $og_warehouse = \IQLRSS\Driver::get_ss_opt( 'global_warehouse' ); 405 $api = new Api\Shipstation(); 406 407 // Grab Warehouse options 408 $api_warehouses = $api->get_warehouses(); 409 if( is_a( $api_warehouses, 'WP_Error' ) ) { 410 $warehouses = array( '' => $api_warehouses->get_error_message() ); 411 } else if( is_array( $api_warehouses ) && ! empty( $api_warehouses ) ) { 412 $warehouses = array_merge( $warehouses, array_combine( 413 array_keys( $api_warehouses ), 414 array_column( $api_warehouses, 'name' ), 415 ) ); 416 } 417 418 // Move the global warehouse to the top. 419 if( ! empty( $og_warehouse ) && isset( $warehouses[ $og_warehouse ] ) ) { 420 $tmp_houses = array_diff_key( $warehouses, array( '' => '' ) ); 421 $warehouses = array_merge( array( 422 '' => $warehouses[ $og_warehouse ] . ' (' . esc_html__( 'Store Global', 'live-rates-for-shipstation' ) . ')', 423 '_woo_default' => $store_warehouse_label, 424 ), $tmp_houses ); 425 } 426 } 427 411 428 $settings = array( 412 429 'title' => array( … … 416 433 'default' => esc_html__( 'ShipStation Rates', 'live-rates-for-shipstation' ), 417 434 'desc_tip' => true, 435 ), 436 'warehouse' => array( 437 'title' => esc_html__( 'Shipping Location', 'live-rates-for-shipstation' ), 438 'type' => 'select', 439 'options' => $warehouses, 440 'description' => esc_html__( 'Select to ship from a different location than the default.', 'live-rates-for-shipstation' ), 418 441 ), 419 442 'minweight' => array( … … 453 476 /** 454 477 * Allow filtering the Shipping Zone settings 455 * 478 * 456 479 * @hook filter 457 * 480 * 458 481 * @param Array $settings 459 482 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 460 * 483 * 461 484 * @return Array $settings 462 485 */ … … 481 504 482 505 ob_start(); 483 include 'assets/views/customboxes-table.php';506 include \IQLRSS\Driver::get_asset_path( 'views/shipping-zone/customboxes-table.php' ); 484 507 return ob_get_clean(); 485 508 … … 590 613 591 614 ob_start(); 592 include 'assets/views/services-table.php';615 include \IQLRSS\Driver::get_asset_path( 'views/shipping-zone/services-table.php' ); 593 616 return ob_get_clean(); 594 617 … … 660 683 } 661 684 685 662 686 /** 663 687 * We don't want to array_filter() since … … 703 727 } 704 728 705 $enabled_services = $this->get_enabled_services(); 706 if( empty( $enabled_services ) ) { 707 $this->log( esc_html__( 'No enabled carrier services found. Please enable carrier services within the shipping zone.', 'live-rates-for-shipstation' ) ); 708 return; 709 } 710 711 $saved_carriers = array_keys( $enabled_services ); 712 if( ! empty( $saved_carriers ) && ! empty( $this->carriers ) ) { 713 $saved_carriers = array_values( array_intersect( $saved_carriers, $this->carriers ) ); 714 } 715 716 $global_adjustment = floatval( \IQLRSS\Driver::get_ss_opt( 'global_adjustment', 0 ) ); 717 $global_adjustment_type = \IQLRSS\Driver::get_ss_opt( 'global_adjustment_type','' ); 718 $global_adjustment_type = ( empty( $global_adjustment_type ) && ! empty( $global_adjustment ) ) ? 'percentage' : $global_adjustment_type; 719 720 $packing_type = $this->get_option( 'packing', 'individual' ); 721 $request = array( 722 'from_country_code' => WC()->countries->get_base_country(), 723 'from_postal_code' => WC()->countries->get_base_postcode(), 724 'from_city_locality' => WC()->countries->get_base_city(), 725 'from_state_province'=> WC()->countries->get_base_state(), 726 727 'to_country_code' => $packages['destination']['country'], 728 'to_postal_code' => $packages['destination']['postcode'], 729 'to_city_locality' => $packages['destination']['city'], 730 'to_state_province' => $packages['destination']['state'], 731 732 'address_residential_indicator' => 'unknown', 733 ); 734 735 $item_requests = array(); 736 $callback = sprintf( 'group_requestsby_%s', str_replace( '-', '_', $packing_type ) ); 737 if( method_exists( $this, $callback ) ) { 738 $item_requests = call_user_func( array( $this, $callback ), $packages['contents'] ); 739 } 729 // Grab the calculator to be filtered. 730 $calculator = new Classes\Shipping_Calculator( $packages, array( 731 'shipping_method' => $this, 732 ) ); 740 733 741 734 742 735 /** 743 * Allow filtering the packages before requesting estimates. 744 * 745 * The returned array should follow this format: 746 * Multi-dimensional Array 747 * 748 * $item_requests = Array( Array( 749 * ~ Required Fields: 750 * '_name' => '$productID|$productName', - This format makes it easy to show the Shop Manager what's packed into the box. 751 * 'dimensions' => array( 752 * 'length => 123, 753 * 'width' => 123, 754 * 'height' => 123, 755 * 'unit' => 'inch', - ShipStation expects a specific string. See \IQLRSS\Core\Api\Shipstation::convert_unit_term( $unit ) 756 * ), 757 * 'weight' => array( 758 * 'value' => 123, 759 * 'unit' => 'pound', - ShipStation expects a specific string. See \IQLRSS\Core\Api\Shipstation::convert_unit_term( $unit ) 760 * ), 761 * 762 * ~ Entirely optional, but the system will try to read them if available. 763 * 'packed' => Array( '$productID|$productName', '$productID|$productName' ), 764 * 'price' => 123, 765 * 'nickname' => 'String' - Displayed to the Shop Owner on the Edit Order page. 766 * 'box_weight' => 123, 767 * 'box_max_weight'=> 123, 768 * 'package_code' => 'ups_ground', 769 * 'carrier_code' => 'ups', - Carrier Code should match what ShipStation expects. I.E. fedex_walleted. This is to group packages with carriers for discounts. 770 * ) ) 771 * 736 * Allow overriding the Shipping Calculator object. 737 * Must inherit IQLRSS\Core\Classes\Shipping_Calculator 738 * 772 739 * @hook filter 773 * 774 * @param Array $item_requests - Array of Package dimensions that the API will use to get rates on. Multidimensional Array.775 * @param Array $packages - The cart contents. See $packages['contents'] for items. 740 * 741 * @param \IQLRSS\Core\Classes\Shipping_Calculator $calculator 742 * @param Array $packages - The cart contents. See $packages['contents'] for items. 776 743 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 777 * 744 * 778 745 * @return Array $settings 779 746 */ 780 $filtered_requests = apply_filters( 'iqlrss/shipping/packages', $item_requests, $packages, $packing_type, $this ); 781 782 // IF the hash doesn't match what was given to the filter, note it in the logs so the store owner will know. 783 $item_req_hash = ( ! empty( $item_requests ) ) ? md5( maybe_serialize( $item_requests ) ) : ''; 784 $filtered_req_hash = ( ! empty( $filtered_requests ) ) ? md5( maybe_serialize( $filtered_requests ) ) : ''; 785 if( $item_req_hash !== $filtered_req_hash ) { 786 $this->log( esc_html__( 'The Shipping packages were modified by a 3rd party using the `iqlrss/shipping/packages` filter hook.', 'live-rates-for-shipstation' ), 'notice' ); 787 } 788 789 /** 790 * We have to return reates per package. 791 * The /rates/estimate endpoint requires less info 792 * and /rates endpoint is way slower. 793 */ 794 $rates = array(); 795 foreach( $filtered_requests as $item_id => $req ) { 796 797 // Create the API request combining the package (weight, dimensions), general request data, and the carrier info. 798 $api_request = array_merge( 799 $req, // Package (weight, dimensions) 800 $request, // General info like to/from address 801 array( // Saved carrier ids 802 'carrier_ids' => $saved_carriers, 803 ) 804 ); 805 806 // Ping the ShipStation API to get rates per Carrier. 807 // Continue - Something went wrong, should be logged on the API side. 808 $available_rates = $this->shipStationApi->get_shipping_estimates( $api_request ); 809 810 if( is_wp_error( $available_rates ) || empty( $available_rates ) ) { 811 continue; 747 $maybe_calc = apply_filters( 'iqlrss/shipping/calculator_object', $calculator, $packages, $this ); 748 if( is_object( $maybe_calc ) && $maybe_calc !== $calculator ) { 749 750 // Override calculator object and log it's change. 751 if( is_subclass_of( $maybe_calc, '\IQLRSS\Core\Classes\Shipping_Calculator' ) ) { 752 753 $calculator = $maybe_calc; 754 $this->log( sprintf( '%s [%s]', 755 esc_html__( 'Shipping Calculations Object overridden.', 'live-rates-for-shipstation' ), 756 get_class( $maybe_calc ) 757 ), 'notice' ); 758 759 // Something went wrong, log that too. 760 } else { 761 762 $this->log( sprintf( '%s [%s]', 763 esc_html__( 'Shipping Calculations Object override failed. Class may not inherit "\IQLRSS\Core\Classes\Shipping_Calculator".', 'live-rates-for-shipstation' ), 764 get_class( $maybe_calc ), 765 'warning' 766 ) ); 767 812 768 } 813 814 // Loop the found rates and setup the WooCommerce rates array for each. 815 foreach( $available_rates as $shiprate ) { 816 817 if( ! isset( $enabled_services[ $shiprate['carrier_id'] ][ $shiprate['code'] ] ) ) { 818 continue; 819 } 820 821 $ratehash = md5( sprintf( '%s%s', $shiprate['code'], $shiprate['carrier_id'] ) ); 822 $service_arr = $enabled_services[ $shiprate['carrier_id'] ][ $shiprate['code'] ]; 823 $cost = floatval( $shiprate['cost'] ); 824 $rate_name = ( isset( $req['_name'] ) ) ? $req['_name'] : ''; 825 $rate_name = ( empty( $rate_name ) && isset( $req['nickname'] ) ) ? $req['nickname'] : $rate_name; 826 $ratemeta = array( 827 '_name'=> $rate_name, // Item products(ID|Name) or box nickname. 828 'rate' => $cost, 829 ); 830 831 // Apply service upcharge 832 if( isset( $service_arr['adjustment'] ) ) { 833 834 /** 835 * Adjustment type could be '' to skip global adjustment. 836 * Defaults to percentage for v1.03 backwards compatibility. 837 */ 838 $adjustment = floatval( $service_arr['adjustment'] ); 839 $adjustment_type = ( isset( $service_arr['adjustment_type'] ) ) ? $service_arr['adjustment_type'] : 'percentage'; 840 841 if( ! empty( $adjustment_type ) && $adjustment > 0 ) { 842 843 $adjustment_cost = ( 'percentage' == $adjustment_type ) ? ( $cost * ( floatval( $adjustment ) / 100 ) ) : floatval( $adjustment ); 844 $ratemeta['adjustment'] = array( 845 'type' => $adjustment_type, 846 'rate' => $adjustment, 847 'cost' => $adjustment_cost, 848 'global'=> false, 849 ); 850 $cost += $adjustment_cost; 851 852 } 853 854 } else if( ! empty( $global_adjustment_type ) && $global_adjustment > 0 ) { 855 856 $adjustment_cost = ( 'percentage' == $global_adjustment_type ) ? ( $cost * ( floatval( $global_adjustment ) / 100 ) ) : floatval( $global_adjustment ); 857 $ratemeta['adjustment'] = array( 858 'type' => $global_adjustment_type, 859 'rate' => $global_adjustment, 860 'cost' => $adjustment_cost, 861 'global'=> true, 862 ); 863 $cost += $adjustment_cost; 864 865 } 866 867 // Loop and add any other shipment amounts. 868 if( ! empty( $shiprate['other_costs'] ) ) { 869 870 $ratemeta['other_costs'] = array(); 871 foreach( $shiprate['other_costs'] as $slug => $cost_arr ) { 872 873 if( empty( $cost_arr['amount'] ) ) continue; 874 $cost += floatval( $cost_arr['amount'] ); 875 $ratemeta['other_costs'][ $slug ] = $cost_arr['amount']; 876 877 } 878 } 879 880 // Maybe a package price 881 if( 'wc-box-packer' == $packing_type && isset( $req['price'] ) && ! empty( $req['price'] ) ) { 882 $cost += floatval( $req['price'] ); 883 $ratemeta['other_costs']['box_price'] = $req['price']; 884 } 885 886 // Maybe apply per item. 887 if( 'individual' == $packing_type ) { 888 $cost *= $packages['contents'][ $item_id ]['quantity']; 889 $ratemeta['qty'] = $packages['contents'][ $item_id ]['quantity']; 890 } 891 892 // Set rate or append the estimated item ship cost. 893 if( ! isset( $rates[ $ratehash ] ) ) { 894 895 $rates[ $ratehash ] = array( 896 'id' => $ratehash, 897 'label' => ( ! empty( $service_arr['nickname'] ) ) ? $service_arr['nickname'] : $shiprate['name'], 898 'package' => $packages, 899 'cost' => array( $cost ), 900 'meta_data' => array( 901 'carrier' => $shiprate['carrier_name'], 902 'service' => $shiprate['name'], 903 'rates' => array(), 904 'boxes' => array(), 905 906 // Private metadata fields must be excluded via filter way above. 907 "_{$this->plugin_prefix}_carrier_id" => $shiprate['carrier_id'], 908 "_{$this->plugin_prefix}_carrier_code" => $shiprate['carrier_code'], 909 "_{$this->plugin_prefix}_service_code" => $shiprate['code'], 910 ), 911 ); 912 913 } else { 914 $rates[ $ratehash ]['cost'][] = $cost; 915 } 916 917 // Merge item rates 918 $rates[ $ratehash ]['meta_data']['rates'] = array_merge( 919 $rates[ $ratehash ]['meta_data']['rates'], 920 array( $ratemeta ), 921 ); 922 923 // Merge item boxes 924 $rates[ $ratehash ]['meta_data']['boxes'] = array_merge( 925 $rates[ $ratehash ]['meta_data']['boxes'], 926 array( $req ), 927 ); 928 769 } 770 771 // Get and Add Rates - EZPZ 772 if( $rates = $calculator->get_rates() ) { 773 foreach( $rates as $rate ) { 774 $this->add_rate( $rate ); 929 775 } 930 931 }932 933 $single_lowest = \IQLRSS\Driver::get_ss_opt( 'return_lowest', 'no' );934 $single_lowest_label = \IQLRSS\Driver::get_ss_opt( 'return_lowest_label', '' );935 936 // Add all shipping rates, let the user decide.937 if( 'no' == $single_lowest || empty( $single_lowest ) ) {938 939 foreach( $rates as $rate_arr ) {940 941 // If more than 1 rate, add the cheapest.942 if( count( $rate_arr['cost'] ) > 1 ) {943 usort( $rate_arr['cost'], fn( $r1, $r2 ) => ( (float)$r1 < (float)$r2 ) ? -1 : 1 );944 $rate_arr['cost'] = (array)array_shift( $rate_arr['cost'] );945 }946 947 // WooCommerce skips serialized data when outputting order item meta, this is a workaround.948 // See hooks above for formatting.949 $rate_arr['meta_data']['rates'] = wp_json_encode( $rate_arr['meta_data']['rates'] );950 $rate_arr['meta_data']['boxes'] = wp_json_encode( $rate_arr['meta_data']['boxes'] );951 952 $this->add_rate( $rate_arr );953 }954 955 // Find the single lowest shipping rate956 } else if( 'yes' == $single_lowest ) {957 958 $lowest = 0;959 $lowest_service = array_key_first( $rates );960 foreach( $rates as $service_id => $rate_arr ) {961 962 $total = array_sum( $rate_arr['cost'] );963 if( 0 == $lowest || $total < $lowest ) {964 $lowest = $total;965 $lowest_service = $service_id;966 }967 }968 969 if( ! empty( $single_lowest_label ) ) {970 $rates[ $lowest_service ]['label'] = $single_lowest_label;971 }972 973 // WooCommerce skips serialized data when outputting order item meta, this is a workaround.974 // See hooks above for formatting.975 $rates[ $lowest_service ]['meta_data']['rates'] = wp_json_encode( $rates[ $lowest_service ]['meta_data']['rates'] );976 $rates[ $lowest_service ]['meta_data']['boxes'] = wp_json_encode( $rates[ $lowest_service ]['meta_data']['boxes'] );977 978 $this->add_rate( $rates[ $lowest_service ] );979 980 776 } 981 777 … … 994 790 995 791 /** 996 * Return an array of API requests which would be for individual products.997 *998 * @param Array $items999 *1000 * @return Array $requests1001 */1002 public function group_requestsby_individual( $items ) {1003 1004 $item_requests = array();1005 $default_weight = $this->get_option( 'minweight', '' );1006 1007 foreach( $items as $item_id => $item ) {1008 1009 // Continue - No shipping needed for product.1010 if( ! $item['data']->needs_shipping() ) {1011 continue;1012 }1013 1014 $request = array(1015 '_name' => sprintf( '%s|%s',1016 $item['data']->get_id(),1017 $item['data']->get_name(),1018 ),1019 'weight' => ( ! empty( $item['data']->get_weight() ) ) ? $item['data']->get_weight() : $default_weight,1020 );1021 $physicals = array_filter( array(1022 'length' => $item['data']->get_length(),1023 'width' => $item['data']->get_width(),1024 'height' => $item['data']->get_height(),1025 ) );1026 1027 // Return Early - Product missing one of the 4 key dimensions.1028 if( count( $physicals ) < 3 || empty( $request['weight'] ) ) {1029 $this->log( sprintf(1030 1031 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */1032 esc_html__( 'Product ID #%1$d missing (%2$s) dimensions. Weight is a minimum requirement. Shipping calculations terminated.', 'live-rates-for-shipstation' ),1033 $item['product_id'],1034 implode( ', ', array_diff_key( array(1035 'length' => 'length',1036 'width' => 'width',1037 'height' => 'height',1038 'weight' => 'weight',1039 ), $physicals + array( 'weight' => $request['weight'] ) ) )1040 ) );1041 1042 return array();1043 }1044 1045 // Set rate request dimensions.1046 sort( $physicals );1047 if( 3 == count( $physicals ) ) {1048 $request['dimensions'] = array(1049 'length' => round( wc_get_dimension( $physicals[2], $this->store_data['dim_unit'] ), 2 ),1050 'width' => round( wc_get_dimension( $physicals[1], $this->store_data['dim_unit'] ), 2 ),1051 'height' => round( wc_get_dimension( $physicals[0], $this->store_data['dim_unit'] ), 2 ),1052 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ),1053 );1054 }1055 1056 // Set rate request weight.1057 if( ! empty( $request['weight'] ) ) {1058 $request['weight'] = array(1059 'value' => (float)round( wc_get_weight( $request['weight'], $this->store_data['weight_unit'] ), 2 ),1060 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),1061 );1062 }1063 1064 $item_requests[ $item_id ] = $request;1065 1066 }1067 1068 return $item_requests;1069 1070 }1071 1072 1073 /**1074 * One Big Box1075 * Group all the products by weight and get rates by total weight.1076 *1077 * @param Array $items1078 *1079 * @return Array $requests1080 */1081 public function group_requestsby_onebox( $items ) {1082 1083 $default_weight = $this->get_option( 'minweight', 0 );1084 $subtype = $this->get_option( 'packing_sub', 'weightonly' );1085 $dimensions = array(1086 'running' => array_combine( array( 'length', 'width', 'height', 'weight' ), array_fill( 0, 4, 0 ) ),1087 'largest' => array_combine( array( 'length', 'width', 'height', 'weight' ), array_fill( 0, 4, 0 ) ),1088 );1089 1090 foreach( $items as $item_id => $item ) {1091 1092 // Continue - No shipping needed for product.1093 if( ! $item['data']->needs_shipping() ) {1094 continue;1095 }1096 1097 $request = array(1098 '_name' => sprintf( '%s|%s',1099 $item['data']->get_id(),1100 $item['data']->get_name(),1101 ),1102 'weight' => ( ! empty( $item['data']->get_weight() ) ) ? $item['data']->get_weight() : $default_weight,1103 );1104 1105 // Return Early - Missing minimum requirement: weight.1106 if( empty( $request['weight'] ) ) {1107 1108 $this->log( sprintf(1109 1110 /* translators: %1$d is the Product ID. */1111 esc_html__( 'Product ID #%1$d missing weight. Shipping Zone weight fallback could not be used. Shipping calculations terminated.', 'live-rates-for-shipstation' ),1112 $item['product_id']1113 ) );1114 1115 return array();1116 1117 }1118 1119 $dimensions['running']['weight'] = $dimensions['running']['weight'] + ( floatval( $request['weight'] ) * $item['quantity'] );1120 $dimensions['running']['height'] = $dimensions['running']['height'] + ( floatval( $item['data']->get_height() ) * $item['quantity'] );1121 $dimensions['largest'] = array(1122 'length' => ( $dimensions['largest']['length'] < $item['data']->get_length() ) ? $item['data']->get_length() : $dimensions['largest']['length'],1123 'width' => ( $dimensions['largest']['width'] < $item['data']->get_width() ) ? $item['data']->get_width() : $dimensions['largest']['width'],1124 'height' => ( $dimensions['largest']['height'] < $item['data']->get_height() ) ? $item['data']->get_height() : $dimensions['largest']['height'],1125 'weight' => ( $dimensions['largest']['weight'] < $request['weight'] ) ? $request['weight'] : $dimensions['largest']['weight'],1126 );1127 1128 }1129 1130 // Return Early - Rates by total weight.1131 if( 'weightonly' == $subtype ) {1132 1133 return array( array(1134 'weight' => array(1135 'value' => (float)round( wc_get_weight( $dimensions['running']['weight'], $this->store_data['weight_unit'] ), 2 ),1136 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),1137 ),1138 ) );1139 1140 }1141 1142 $physicals = array_filter( array(1143 'length' => $dimensions['largest']['length'],1144 'width' => $dimensions['largest']['width'],1145 'height' => $dimensions['running']['height'],1146 'weight' => $dimensions['running']['weight'],1147 ) );1148 1149 // Return Early - Error - Missing dimensions to work with.1150 if( $physicals < 4 ) {1151 1152 $this->log( sprintf(1153 1154 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */1155 esc_html__( 'OneBox rate requestion missing dimensions (%1$s). Weight is a minimum requirement. Shipping calculations terminated.', 'live-rates-for-shipstation' ),1156 implode( ', ', array_diff_key( array(1157 'length' => 'length',1158 'width' => 'width',1159 'height' => 'height',1160 'weight' => 'weight',1161 ), $physicals ) )1162 ) );1163 1164 return array();1165 1166 }1167 1168 // Default - Stacked Verticially1169 return array( array(1170 'weight' => array(1171 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),1172 'value' => (float)round( wc_get_weight( $physicals['weight'], $this->store_data['weight_unit'] ), 2 ),1173 ),1174 'dimensions' => array(1175 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ),1176 1177 // Largest1178 'length' => round( wc_get_dimension( $physicals['length'], $this->store_data['dim_unit'] ), 2 ),1179 'width' => round( wc_get_dimension( $physicals['width'], $this->store_data['dim_unit'] ), 2 ),1180 1181 // Running1182 'height' => round( wc_get_dimension( $physicals['height'], $this->store_data['dim_unit'] ), 2 ),1183 ),1184 ) );1185 1186 }1187 1188 1189 /**1190 * Return an array of API requests for custom packed boxes.1191 * Shoutout to Mike Jolly & Co.1192 *1193 * @param Array $items1194 *1195 * @return Array $requests1196 */1197 public function group_requestsby_wc_box_packer( $items ) {1198 1199 $item_requests = array();1200 $boxes = $this->get_option( 'customboxes', array() );1201 $default_weight = $this->get_option( 'minweight', '' );1202 1203 /* Return Early - No custom boxes found. */1204 if( empty( $boxes ) ) {1205 $this->log( esc_html__( 'Custom Boxes selected, but no boxes found. Items packed individually', 'live-rates-for-shipstation' ), 'warning' );1206 return $this->group_requestsby_individual( $items );1207 }1208 1209 if( ! class_exists( '\IQRLSS\WC_Box_Packer\WC_Boxpack' ) ) {1210 include_once 'wc-box-packer/class-wc-boxpack.php';1211 }1212 1213 // Setup the WC_Boxpack boxes based on user submitted custom boxes.1214 $wc_boxpack = new WC_Box_Packer\WC_Boxpack();1215 foreach( $boxes as $box ) {1216 if( empty( $box['active'] ) ) continue;1217 $wc_boxpack->add_box( $box );1218 }1219 1220 // Loop the items, grabs their dimensions, and assocaite them with WC_Boxpack for future packing.1221 foreach( $items as $item_id => $item ) {1222 if( ! $item['data']->needs_shipping() ) continue;1223 1224 $weight = ( ! empty( $item['data']->get_weight() ) ) ? $item['data']->get_weight() : $default_weight;1225 $data = array(1226 'weight' => (float)round( wc_get_weight( $weight, $this->store_data['weight_unit'] ), 2 ),1227 );1228 $physicals = array_filter( array(1229 'length' => $item['data']->get_length(),1230 'width' => $item['data']->get_width(),1231 'height' => $item['data']->get_height(),1232 ) );1233 1234 // Return Early - Product missing one of the 4 key dimensions.1235 if( count( $physicals ) < 3 && empty( $data['weight'] ) ) {1236 $this->log( sprintf(1237 1238 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */1239 esc_html__( 'Product ID #%1$d missing (%2$s) dimensions and no weight found. Shipping calculations terminated.', 'live-rates-for-shipstation' ),1240 $item['product_id'],1241 implode( ', ', array_diff_key( array(1242 'width' => 'width',1243 'height' => 'height',1244 'length' => 'length',1245 ), $physicals ) )1246 ) );1247 return array();1248 }1249 1250 sort( $physicals );1251 $data = array(1252 'length' => round( wc_get_dimension( $physicals[2], $this->store_data['dim_unit'] ), 2 ),1253 'width' => round( wc_get_dimension( $physicals[1], $this->store_data['dim_unit'] ), 2 ),1254 'height' => round( wc_get_dimension( $physicals[0], $this->store_data['dim_unit'] ), 2 ),1255 'weight' => round( wc_get_weight( $data['weight'], $this->store_data['weight_unit'] ), 2 ),1256 );1257 1258 // Pack Products1259 for( $i = 0; $i < $item['quantity']; $i++ ) {1260 $wc_boxpack->add_item(1261 $data['length'],1262 $data['width'],1263 $data['height'],1264 $data['weight'],1265 $item['data']->get_price(),1266 array(1267 '_name' => sprintf( '%s|%s',1268 $item['data']->get_id(),1269 $item['data']->get_name(),1270 ),1271 ),1272 );1273 }1274 }1275 1276 // Pack it up, missions over.1277 $wc_boxpack->pack();1278 $wc_box_packages = $wc_boxpack->get_packages();1279 $box_log = array();1280 1281 // Delivery!1282 foreach( $wc_box_packages as $key => $package ) {1283 1284 $packed_items = ( is_array( $package->packed ) ) ? array_map( function( $item ) { return $item->meta['_name']; }, $package->packed ) : array();1285 $item_requests[] = array(1286 'weight' => array(1287 'value' => round( $package->weight, 2 ),1288 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),1289 ),1290 'dimensions' => array(1291 'length' => round( $package->length, 2 ),1292 'width' => round( $package->width, 2 ),1293 'height' => round( $package->height, 2 ),1294 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ),1295 ),1296 'packed' => $packed_items,1297 'price' => ( ! empty( $package->data ) ) ? $package->data['price'] : 0,1298 'nickname' => ( ! empty( $package->data ) ) ? $package->data['nickname'] : '',1299 'box_weight' => ( ! empty( $package->data ) ) ? $package->data['weight'] : 0,1300 'box_max_weight'=> ( ! empty( $package->data ) ) ? $package->data['weight_max'] : 0,1301 'package_code' => ( ! empty( $package->data ) ) ? $package->data['preset'] : '',1302 'carrier_code' => ( ! empty( $package->data ) ) ? $package->data['carrier_code'] : '',1303 );1304 1305 $box_log[] = array(1306 'is_packed' => boolval( empty( $package->unpacked ) ),1307 'item_count' => count( $package->packed ),1308 'items' => $packed_items,1309 'box_dimensions' => sprintf( '%s x %s x %s | %s | %s', $package->length, $package->width, $package->height, $package->weight, $package->volume ),1310 'box_dim_key' => sprintf( '%s x %s x %s | %s | %s',1311 esc_html__( 'Length', 'live-rates-for-shipstation' ),1312 esc_html__( 'Width', 'live-rates-for-shipstation' ),1313 esc_html__( 'Height', 'live-rates-for-shipstation' ),1314 esc_html__( 'Weight', 'live-rates-for-shipstation' ),1315 esc_html__( 'Volume', 'live-rates-for-shipstation' ),1316 ),1317 'max_volume' => floatval( $package->width * $package->height * $package->length ),1318 'data' => ( ! empty( $package->data ) ) ? $package->data : array(),1319 );1320 1321 }1322 1323 if( ! empty( $box_log ) ) {1324 $this->log( esc_html__( 'Custom Boxes Packed', 'live-rates-for-shipstation' ), 'info', $box_log );1325 }1326 1327 return $item_requests;1328 1329 }1330 1331 1332 /**1333 792 * Set the rates based on cached packages. 1334 793 * … … 1343 802 protected function check_packages_rate_cache( $packages ) { 1344 803 804 805 /** 806 * Maybe skip cart caches. 807 * Do note that WooCommerce makes multiple calls to the cart / calculations. 808 * Disabling this may result in many more API calls than expected. 809 * 810 * @hook filter 811 * 812 * @param Bolean TRUE 813 * 814 * @return Boolean 815 */ 816 // Return Early - Filter Skips Cache. 817 if( true !== apply_filters( 'iqlrss/cache/cart_rates', true ) ) return; 818 1345 819 $session = WC()->session->get( $this->plugin_prefix . '_packages', array() ); 1346 820 $cleartime = get_transient( \IQLRSS\Driver::plugin_prefix( 'wcs_timeout' ) ); … … 1359 833 $size = count( $packages ); 1360 834 for( $i = 0; $i < $size; $i++ ) { 1361 1362 835 $cache = WC()->session->get( 'shipping_for_package_' . $i, false ); 1363 if( empty( $cache ) || ! is_array( $cache ) ) { 1364 break; 1365 } 836 if( empty( $cache ) || ! is_array( $cache ) ) break; 1366 837 $this->rates = array_merge( $cache['rates'], $this->rates ); 1367 1368 838 } 1369 839 … … 1469 939 1470 940 /** 1471 * Return an m-array of enabled services grouped by carrier key.1472 *1473 * @return Array1474 */1475 public function get_enabled_services() {1476 1477 $enabled = array();1478 $saved_services = $this->get_option( 'services', array() );1479 if( empty( $saved_services ) ) return $enabled;1480 1481 foreach( $saved_services as $c => $sa ) {1482 foreach( $sa as $sk => $s ) {1483 if( ! isset( $s['enabled'] ) || ! $s['enabled'] ) continue;1484 $enabled[ $c ][ $sk ] = $s;1485 }1486 }1487 1488 return $enabled;1489 1490 }1491 1492 1493 /**1494 941 * Convert a WooCommerce unit to a ShipStation unit. 1495 * 942 * 1496 943 * @param String $unit 1497 * 944 * 1498 945 * @return String $new_unit 1499 946 */ … … 1515 962 } 1516 963 1517 $global_carriers= $this->shipStationApi->get_carriers(); 1518 $carrier_codes = wp_list_pluck( $global_carriers, 'carrier_code' ); 1519 $carrier_codes = array_intersect_key( $carrier_codes, array_flip( $this->carriers ) ); 964 $global_carriers = $this->shipStationApi->get_carriers(); 965 $carrier_codes = wp_list_pluck( $global_carriers, 'carrier_code' ); 966 $carrier_codes = array_intersect_key( $carrier_codes, array_flip( $this->carriers ) ); 967 $carrier_packages = array(); 1520 968 1521 969 $data = array( … … 1534 982 ); 1535 983 984 // Append ShipStation Packages 985 $sspackages = $this->shipStationApi->get_packages(); 986 if( ! is_wp_error( $sspackages ) && ! empty( $sspackages ) ) { 987 988 $carrier_packages['shipstation'] = array( 989 'label' => esc_html__( 'ShipStation' ), 990 'packages' => array(), 991 ); 992 993 foreach( $sspackages as $package ) { 994 $carrier_packages['shipstation']['packages'][] = array( 995 'label' => $package['name'], 996 'code' => $package['package_id'], 997 'length' => $package['dimensions']['length'], 998 'width' => $package['dimensions']['width'], 999 'height' => $package['dimensions']['height'], 1000 'weight_max' => '', 1001 'carrier_code' => '', 1002 ); 1003 } 1004 } 1005 1536 1006 // Append Translated Labels 1537 $carrier_packages = array();1538 1007 foreach( $data as $carrier_code => &$carriers ) { 1539 1008 … … 1640 1109 } 1641 1110 1642 1643 /**1644 * Log error in WooCommerce1645 * Passthru method - log what's given and give it back.1646 * Could make a good Trait1647 *1648 * @param Mixed $error - String or WP_Error1649 * @param String $level - WooCommerce level (debug|info|notice|warning|error|critical|alert|emergency)1650 * @param Array $context1651 *1652 * @return Mixed - Return the error back.1653 */1654 protected function log( $error, $level = 'debug', $context = array() ) {1655 1656 if( ! \IQLRSS\Driver::get_ss_opt( 'logging_enabled', 0, true ) ) {1657 return $error;1658 }1659 1660 if( is_wp_error( $error ) ) {1661 $error_msg = sprintf( '(%s) %s', $error->get_error_code(), $error->get_error_message() );1662 } else {1663 $error_msg = $error;1664 }1665 1666 if( class_exists( '\WC_Logger' ) ) {1667 1668 if( null === $this->logger ) {1669 $this->logger = \wc_get_logger();1670 }1671 1672 $this->logger->log( $level, $error_msg, array_merge( $context, array( 'source' => 'live-rates-for-shipstation' ) ) );1673 1674 }1675 1676 return $error;1677 1678 }1679 1680 1111 } -
live-rates-for-shipstation/tags/1.2.0/core/traits/logger.php
r3442676 r3452263 22 22 * @return Mixed - Return the error back. 23 23 */ 24 protected function log( $error, $level = ' debug', $context = array() ) {24 protected function log( $error, $level = 'info', $context = array() ) { 25 25 26 26 if( ! \IQLRSS\Driver::get_ss_opt( 'logging_enabled', 0, true ) ) { … … 37 37 if( class_exists( '\WC_Logger' ) ) { 38 38 39 /** 40 * The WC_Logger does not handle double quotes well. 41 * This will convert double quotes to faux: " -> '' 42 */ 43 array_walk_recursive( $context, function( &$val ) { 44 $val = ( is_string( $val ) ) ? str_replace( '"', "''", $val ) : $val; 45 } ); 46 39 47 $logger = \wc_get_logger(); 40 48 $logger->log( $level, $error_msg, array_merge( $context, array( 'source' => 'live-rates-for-shipstation' ) ) ); -
live-rates-for-shipstation/tags/1.2.0/live-rates-for-shipstation.php
r3442676 r3452263 4 4 * Plugin URI: https://iqcomputing.com/contact/ 5 5 * Description: ShipStation shipping method with live rates. 6 * Version: 1. 1.27 * Requ ries at least: 6.26 * Version: 1.2.0 7 * Requires at least: 6.2 8 8 * Author: IQComputing 9 9 * Author URI: https://iqcomputing.com/ … … 26 26 * @var String 27 27 */ 28 protected static $version = '1. 1.2';28 protected static $version = '1.2.0'; 29 29 30 30 … … 229 229 Core\Rest_Router::initialize(); 230 230 Core\Settings_Shipstation::initialize(); 231 Core\Admin_Edit_Order::initialize(); 232 231 233 } 232 234 … … 235 237 236 238 /** 237 * Class Autoloader 238 * 239 * @param String $class 239 * Autoload and Drive! 240 240 */ 241 spl_autoload_register( function( $class ) {242 243 if( false === strpos( $class, __NAMESPACE__ . '\\' ) ) {244 return $class;245 }246 247 $class_path = str_replace( __NAMESPACE__ . '\\', '', $class );248 $class_path = str_replace( '_', '-', strtolower( $class_path ) );249 $class_path = str_replace( '\\', '/', $class_path );250 $file_path = wp_normalize_path( sprintf( '%s/%s',251 rtrim( plugin_dir_path( __FILE__ ), '\\/' ),252 $class_path . '.php'253 ) );254 255 if( file_exists( $file_path ) ) {256 require_once $file_path;257 }258 259 } );260 241 require_once rtrim( __DIR__, '\\/' ) . '/_autoload.php'; 261 242 add_action( 'plugins_loaded', array( '\IQLRSS\Driver', 'drive' ), 8 ); -
live-rates-for-shipstation/tags/1.2.0/readme.txt
r3442676 r3452263 4 4 Requires at least: 5.9 5 5 Tested up to: 6.8 6 Stable tag: 1. 1.26 Stable tag: 1.2.0 7 7 License: GPLv3 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 51 51 == Changelog == 52 52 53 = 1.2.0 (2026-02-02) = 54 * Adds Warehouse Support (Global and Zone based). 55 * Adds ShipStation Packages into Custom Packages on a Shipping Zone. 56 * Moves shipping calculations into a dedicated class with filer hook to override. 57 * New `iqlrss/cache/shipstation` filter hook. 58 * New `iqlrss/cache/shipstation_expires` filter hook. 59 * New `iqlrss/cache/cart_rates` filter hook. 60 * New `iqlrss/shipping/calculator_object` filter hook. 61 53 62 = 1.1.2 (2026-01-19) = 54 63 * Patched an issue where rate caching would not account for a destination change. … … 60 69 = 1.1.1 (2025-12-04) = 61 70 * Fixed JS conflict with WordPress 6.9 (nice!) 62 63 = 1.1.0 (2025-12-01) =64 * Redux the Custom Packaging screen and options.65 * Packing option for Weight Only.66 * Packing option for Stacked Vertically.67 * Packing option for default product weight.68 * Custom Package Presets from UPS, FedEx, and USPS.69 * New filter hook for Shipping Zone Settings `iqlrss/zone/settings`. Useful for managing Product Packing options.70 * New filter hook for Shipping Zone Settings `iqlrss/zone/package_presets`. Useful for managing Custom Package presets.71 * New filter hook for Shipping Estimates `iqlrss/shipping/packages`. Useful for modifying what gets sent to ShipStation API for retrieving shipping estimates. -
live-rates-for-shipstation/trunk/README.md
r3407166 r3452263 42 42 43 43 This is a free plugin entirely volunteer run. While we will ensure that the plugin is up-to-date for any security issues or breakfixes, we cannot commit to supporting any customizations to the plugin or feature requests, but welcome these questions and requests through the [WordPress Support Forums](https://wordpress.org/support/plugin/live-rates-for-shipstation/) for our team to consider in a future release of the plugin. We recommend using the WordPress Forums for all inquiries. 44 45 ## Filter Hooks 46 47 - `iqlrss/cache/cart_rates` 48 - `iqlrss/cache/shipstation` 49 - `iqlrss/cache/shipstation_expires` 50 - `iqlrss/shipping/calculator_object` 51 - `iqlrss/shipping/packages` 52 - `iqlrss/zone/package_presets` 53 - `iqlrss/zone/settings` -
live-rates-for-shipstation/trunk/_autoload.php
r3442676 r3452263 21 21 ) ); 22 22 23 /* Vroom! */ 24 if( 'driver' === $class_path ) { 25 $file_path = wp_normalize_path( sprintf( '%s/%s', 26 rtrim( plugin_dir_path( __FILE__ ), '\\/' ), 27 'live-rates-for-shipstation.php' 28 ) ); 29 } 30 23 31 if( file_exists( $file_path ) ) { 24 32 require_once $file_path; -
live-rates-for-shipstation/trunk/_stallation.php
r3407166 r3452263 20 20 21 21 /** 22 * Unin tsall Plugin22 * Uninstall Plugin 23 23 */ 24 24 public static function uninstall() { -
live-rates-for-shipstation/trunk/changelog.txt
r3442676 r3452263 2 2 3 3 This is a brief text document keeping track of changes to the plugin. For a full history, see the Github Repository. 4 5 = 1.2.0 = 6 7 Relase Date: Eventually. 8 9 * Overview 10 * Shipping Calculations moved to it's own Class for better PHPUnit testing. 11 * See unit-tests branch; work-in-progress. 12 * ShipStation Warehouses are now supported Globally and per Shipping Zone. 13 * Defaults to WooCommerce Store. Global overrides Store. Zone overrides Global. 14 * ShipStation Packages are now integrated into Custom Packages. 15 * Multiple new filter hooks added to manage caching, warehouses, and Shipping Calculator object. 16 17 * Code Updates 18 * Filter Hook `iqlrss/cache/shipstation` 19 * Boolean to disable caching for API requests. 20 * Filter Hook `iqlrss/cache/shipstation_expires` 21 * Integer seconds for how long to cache package rates for a customer. 22 * 1 Week is the default. 23 * Filter Hook `iqlrss/cache/cart_rates` 24 * Boolean to diable caching at a cart level. 25 * This is not recommended to disable since WC (sometimes) makes multiple calls to the cart per page load, but the option is available if you have a usecase. 26 * Fitler Hook `iqlrss/shipping/calculator_object` 27 * Expects a \IQLRSS\Core\Classes\Shipping_Calculator Object. 28 * Must inherit the IQLRSS Calculator to be valid. 29 * The shipping method calculations had an overhaul and broken into multiple methods of the new Shipping Calculator object. 30 * This is for PHPUnit testing and to make it easier for developers to override specific calculator functionality for their usecases. 4 31 5 32 = 1.1.2 = … … 15 42 = 1.1.1 = 16 43 17 Rel ase Date: December 04, 202544 Release Date: December 04, 2025 18 45 19 46 * Overview … … 23 50 = 1.1.0 = 24 51 25 Rel ase Date: December 01, 202552 Release Date: December 01, 2025 26 53 27 54 * Overview … … 58 85 = 1.0.8 = 59 86 60 Rel ase Date: October 10, 202587 Release Date: October 10, 2025 61 88 62 89 * Overview … … 74 101 * Ugh, the Block Editor + WooCommerce makes _multiple_ async requests to shipping calculations which would re-trigger things unnecessarily. 75 102 * The new methodology uses the WC()->session to automatically return the known rates if the cart has not changed. 76 * This is a notic able increase in speed when browsing the shop when your cart hasn't necessarily changed.77 * Caching layer also prevents multiple / dupl ciate API requests being logged which is nice.103 * This is a noticeable increase in speed when browsing the shop when your cart hasn't necessarily changed. 104 * Caching layer also prevents multiple / duplicate API requests being logged which is nice. 78 105 79 106 * Code Updates … … 83 110 = 1.0.7 = 84 111 85 Rel ase Date: October 08, 2025112 Release Date: October 08, 2025 86 113 87 114 * Overview … … 89 116 * This denotes what items got what rates, the adjustments, where the adjustments come from. 90 117 * Patches issue on Shipping Zone where WP_Error was treated as an Exception 91 * Starts to nor amlize code for carrier_id and carrier_code.118 * Starts to normalize code for carrier_id and carrier_code. 92 119 * Doing this makes it easier to integrate the v1 API. 93 120 * The Carrier ID is the ShipStation `se-` code. … … 102 129 = 1.0.6 = 103 130 104 Rel ase Date: September 22, 2025131 Release Date: September 22, 2025 105 132 106 133 * Overview … … 109 136 = 1.0.5 = 110 137 111 Rel ase Date: September 16, 2025138 Release Date: September 16, 2025 112 139 113 140 * Overview … … 135 162 = 1.0.4 = 136 163 137 Rel ase Date: September 15, 2025164 Release Date: September 15, 2025 138 165 139 166 * Overview … … 153 180 = 1.0.3 = 154 181 155 Rel ase Date: August 05, 2025182 Release Date: August 05, 2025 156 183 157 184 * Overview … … 165 192 = 1.0.2 = 166 193 167 Rel ase Date: August 04, 2025194 Release Date: August 04, 2025 168 195 169 196 * Overview … … 177 204 = 1.0.1 = 178 205 179 Rel ase Date: August 01, 2025206 Release Date: August 01, 2025 180 207 181 208 * Overview -
live-rates-for-shipstation/trunk/core/api/shipstation.php
r3407166 r3452263 2 2 /** 3 3 * ShipStation API Helper 4 * 5 * @link https://docs.shipstation.com/openapi 4 6 * 5 7 * Carrier ID : se-* … … 10 12 */ 11 13 namespace IQLRSS\Core\Api; 14 use \IQLRSS\Core\Traits; 12 15 13 16 if( ! defined( 'ABSPATH' ) ) { … … 18 21 19 22 /** 23 * Inherit logger traits 24 */ 25 use Traits\Logger; 26 27 28 /** 20 29 * Skip cache check 21 30 * … … 67 76 $this->prefix = \IQLRSS\Driver::get( 'slug' ); 68 77 $this->key = \IQLRSS\Driver::get_ss_opt( 'api_key', '' ); 69 $this->skip_cache = (boolean)$skip_cache; 70 $this->cache_time = defined( 'WEEK_IN_SECONDS' ) ? WEEK_IN_SECONDS : 604800; 78 79 80 /** 81 * Skip caching for the API. 82 * 83 * @hook filter 84 * 85 * @param Bolean FALSE 86 * 87 * @return Boolean 88 */ 89 $this->skip_cache = (boolean)apply_filters( 'iqlrss/cache/shipstation', $skip_cache, $this ); 90 91 92 /** 93 * Allow filtering the cache time. 94 * 95 * @see https://codex.wordpress.org/Easier_Expression_of_Time_Constants 96 * 97 * @hook filter 98 * 99 * @param Integer $cache_time - Week in seconds. 100 * 101 * @return Boolean 102 */ 103 $cache_time = apply_filters( 'iqlrss/cache/shipstation_expires', $this->cache_time, $this ); 104 $this->cache_time = ( is_numeric( $cache_time ) ) ? absint( $cache_time ) : $this->cache_time; 71 105 72 106 } … … 101 135 // Return Early - Something went wrong getting carriers. 102 136 } else if( ! isset( $carriers[ $carrier_code ] ) ) { 103 return $this->log( new \WP_Error( 404, esc_html__( 'Could not find carrier information.', 'live-rates-for-shipstation' ) ) );137 return $this->log( new \WP_Error( 404, esc_html__( 'Could not find carrier information.', 'live-rates-for-shipstation' ) ), 'warning' ); 104 138 } 105 139 … … 233 267 * @todo Look into `delivery_days` field. UPS has, is it carrier consistent? 234 268 * 235 * @param Array $est_opts 236 * 237 * @return Array|WP_Error 238 */ 239 public function get_shipping_estimates( $est_opts ) { 240 241 $body = $this->make_request( 'post', 'rates/estimate', $est_opts ); 269 * @link https://docs.shipstation.com/openapi/rates/calculate_rates 270 * @link https://docs.shipstation.com/openapi/rates/estimate_rates 271 * 272 * @param Array $api_args - See ShipStation API docs for required fields. 273 * 274 * @return Array|WP_Error 275 */ 276 public function get_shipping_estimates( $api_args ) { 277 278 $body = $this->make_request( 'post', 'rates/estimate', $api_args ); 242 279 243 280 // Return Early - API Request error - see logs. … … 284 321 285 322 /** 286 * Create a new Shipment 287 * 288 * @param Array $args 289 * 290 * @return Array $data 291 */ 292 public function create_shipments( $args ) { 293 294 $body = $this->make_request( 'post', 'shipments', array( 'shipments' => $args ) ); 323 * Return a single warehouse as a flat array of key value pairs. 324 * 325 * @param String $warehouse_id - Shipstation specific reference code. 326 * 327 * @return Array|WP_Error 328 */ 329 public function get_warehouse( $warehouse_id ) { 330 331 $warehouses = $this->get_warehouses(); 332 if( is_wp_error( $warehouses ) || empty( $warehouses ) ) { 333 return $warehouses; 334 } 335 336 return ( isset( $warehouses[ $warehouse_id ] ) ) ? $warehouses[ $warehouse_id ] : array(); 337 338 } 339 340 341 /** 342 * Return an array of Warehouses. 343 * 344 * @link https://docs.shipstation.com/openapi/warehouses/list_warehouses 345 * 346 * @return Array|WP_Error 347 */ 348 public function get_warehouses() { 349 350 $trans_key = $this->prefix_key( 'warehouses' ); 351 $warehouses = get_transient( $trans_key ); 352 353 if( empty( $warehouses ) || $this->skip_cache ) { 354 355 $body = $this->make_request( 'get', 'warehouses' ); 356 357 // Return Early - API Request error - see logs. 358 if( is_wp_error( $body ) ) { 359 return $body; 360 } 361 362 // Return Early - No Warehouses to work with. 363 if( empty( $body['warehouses'] ) ) { 364 return array(); 365 } 366 367 // We do need most the Warehouse data, but not all. 368 $warehouses = array(); 369 foreach( $body['warehouses'] as $warehouse_data ) { 370 371 $warehouse = array_intersect_key( $warehouse_data, array_flip( array( 372 'warehouse_id', 373 'is_default', 374 'name', 375 'origin_address', 376 'return_address', 377 ) ) ); 378 379 if( $warehouse['is_default'] ) { 380 $warehouse['name'] .= ' (' . esc_html__( 'ShipStation Default', 'live-rates-for-shipstation' ) . ')'; 381 } 382 383 $warehouses[ $warehouse['warehouse_id'] ] = $warehouse; 384 385 } 386 387 // Cache Warehouse data. 388 if( ! empty( $warehouses ) ) { 389 set_transient( $trans_key, $warehouses, $this->cache_time ); 390 } 391 } 392 393 return $warehouses; 394 395 } 396 397 398 /** 399 * Return a single package by ID. 400 * 401 * @param String $package_id - Shipstation specific reference code. 402 * 403 * @return Array|WP_Error 404 */ 405 public function get_package( $package_id ) { 406 407 $packages = $this->get_packages(); 408 if( is_wp_error( $packages ) || empty( $packages ) ) { 409 return $packages; 410 } 411 412 return ( isset( $packages[ $package_id ] ) ) ? $packages[ $package_id ] : array(); 413 414 } 415 416 417 /** 418 * Return an array of Packages 419 * 420 * @link https://docs.shipstation.com/openapi/package_types/list_package_types 421 * 422 * @return Array|WP_Error 423 */ 424 public function get_packages() { 425 426 $trans_key = $this->prefix_key( 'packages' ); 427 $packages = get_transient( $trans_key ); 428 429 if( empty( $packages ) || $this->skip_cache ) { 430 431 $body = $this->make_request( 'get', 'packages' ); 432 433 // Return Early - API Request error - see logs. 434 if( is_wp_error( $body ) ) { 435 return $body; 436 } 437 438 // Return Early - No Custom Packages to work with. 439 if( empty( $body['packages'] ) ) { 440 return array(); 441 } 442 443 // We do need most the Package data, just id, name, dimensions - ezpz. 444 $packages = array(); 445 foreach( $body['packages'] as $package_data ) { 446 447 $package = array_intersect_key( $package_data, array_flip( array( 448 'package_id', 449 'name', 450 'dimensions', 451 ) ) ); 452 453 $packages[ $package['package_id'] ] = $package; 454 455 } 456 457 // Cache Warehouse data. 458 if( ! empty( $packages ) ) { 459 set_transient( $trans_key, $packages, $this->cache_time ); 460 } 461 } 462 463 return $packages; 464 465 } 466 467 468 /** 469 * Purchase a shipping label by a carrier. 470 * 471 * @link https://docs.shipstation.com/openapi/labels/create_label 472 * 473 * @param Array $api_args - See ShipStation API docs for required fields. 474 * @param Boolean $test_label - Whether or not to create a test label. 475 * 476 * @return Array|WP_Error 477 */ 478 public function purchase_shipping_label( $api_args, $test_label = false ) { 479 480 if( $test_label ) $api_args['test_label'] = true; 481 $body = $this->make_request( 'post', 'labels', $api_args ); 295 482 296 483 // Return Early - API Request error - see logs. … … 299 486 } 300 487 301 /** 302 * API returns no errors but also doesn't do anything in ShipStation. 303 */ 304 $data = $body; 488 $data = array(); 305 489 306 490 return $data; … … 309 493 310 494 311 312 495 /** 313 496 * Create Shipments from given WC_Orders. … … 317 500 * @return Array|WP_Error 318 501 */ 319 public function create_shipments_from_wc_orders( $wc_orders ) {502 public function shipment_args_from_wc_orders( $wc_orders ) { 320 503 321 504 $data = array(); … … 325 508 326 509 $shipments = array(); 327 foreach( $wc_orders as $wc_order ) {510 foreach( (array)$wc_orders as $wc_order ) { 328 511 329 512 // Skip … … 337 520 $order_items = $wc_order->get_items(); 338 521 $order_item_ship = $wc_order->get_items( 'shipping' ); 339 $order_item_ship = ( ! empty( $order_item_ship ) ) ? $order_item_ship[ array_key_first( $order_item_ship ) ] : null; 340 522 $shipmentItem = ( ! empty( $order_item_ship ) ) ? $order_item_ship[ array_key_first( $order_item_ship ) ] : null; 523 524 // Return Early - No shipping item found. 525 if( null === $shipmentItem ) { 526 return new \WP_Error( 400, esc_html__( 'No shipping item found in order.', 'live-rates-for-shipstation' ) ); 527 } 528 529 // Shipment args I'll eventually need 530 $others = array( 531 'hold_until_date' => 'Y-m-d UTC', // ShipStation will hold the shipment until this date. Maybe leave blank, but could impact preorder items? 532 'ship_by_date' => 'Y-m-d UTC', // Purely informational - useful for store owners collecting additional data. 533 'ship_date' => 'Y-m-d UTC', // The date the shipment is actually shipped. Defaults to current date. 534 'requested_shipment_service' => 'carrier_service_code', // ex. ups_ground 535 'warehouse_id' => 'se-*', // ShipStation warehouse association: WC_Order Label Override > Shipping Zone > Global. We can skip ship_from if we have this. 536 'return_to' => array(), // Return to address info - need settings for this. 537 'advanced_options' => array(), // Advanced options - might need settings for this. 538 ); 539 540 // Default ShipStation Shipment Array 341 541 $shipment = array( 342 'validate_address' => 'no_validation', 343 'carrier_id' => $order_item_ship->get_meta( '_iqlrss_carrier_id', true ), 344 'store_id' => \IQLRSS\Driver::get_ss_opt( 'store_id' ), 542 'external_order_id' => $wc_order->get_id(), 543 'order_source_code'=> 'woocommerce', 544 'carrier_id' => $shipmentItem->get_meta( '_iqlrss_carrier_id', true ), 545 'service_code' => $shipmentItem->get_meta( '_iqlrss_service_code', true ), 345 546 'shipping_paid' => array( 346 547 'currency' => $wc_order->get_currency(), … … 377 578 ); 378 579 379 $shipment['items'] = array(); 580 // Add Packages 581 // $packages = $shipmentItem->get_meta( 'boxes', true ); 582 // if( ! empty( $packages ) && is_array( $packages ) ) { 583 // foreach( $packages as $package ) { 584 585 // $shipment['packages'][] = array( 586 // 'package_code' => $ship_package['packageCode'], 587 // 'package_name' => $ship_package['packageName'], 588 // 'weight' => array( 589 // 'value' => $ship_package['weight']['value'], 590 // 'unit' => $ship_package['weight']['unit'], 591 // ), 592 // 'dimensions' => array( 593 // 'length' => $ship_package['dimensions']['length'], 594 // 'width' => $ship_package['dimensions']['width'], 595 // 'height' => $ship_package['dimensions']['height'], 596 // 'unit' => $ship_package['dimensions']['unit'], 597 // ), 598 // ); 599 600 // } 601 // } 602 603 // Add Order Items 380 604 foreach( $shipstation_order_arr['items'] as $ship_item ) { 381 605 … … 434 658 } 435 659 436 return $this->create_shipments( $shipments );437 438 660 } 439 661 … … 506 728 // Return Early - API encountered an error. 507 729 if( is_wp_error( $request ) ) { 508 return $this->log( $request );730 return $this->log( $request, 'error' ); 509 731 } else if( 200 != $code || ! is_array( $body ) ) { 510 732 … … 524 746 } 525 747 526 return $this->log( new \WP_Error( $err_code, $err_msg ) );748 return $this->log( new \WP_Error( $err_code, $err_msg ), 'error' ); 527 749 } 528 750 529 751 // Log API Request Result 530 752 /* translators: %s is the API endpoint (example: carriers/rates). */ 531 $this->log( sprintf( esc_html__( 'ShipStation API Request to %s', 'live-rates-for-shipstation' ), $endpoint ), ' info', array(753 $this->log( sprintf( esc_html__( 'ShipStation API Request to %s', 'live-rates-for-shipstation' ), $endpoint ), 'debug', array( 532 754 'args' => $args, 533 755 'code' => $code, … … 575 797 } 576 798 577 578 /**579 * Log error in WooCommerce580 * Passthru method - log what's given and give it back.581 *582 * @param Mixed $error - String or WP_Error583 * @param String $level - WooCommerce level (debug|info|notice|warning|error|critical|alert|emergency)584 * @param Array $context585 *586 * @return Mixed - Return the error back.587 */588 protected function log( $error, $level = 'debug', $context = array() ) {589 590 if( ! \IQLRSS\Driver::get_ss_opt( 'logging_enabled', 0, true ) ) {591 return $error;592 }593 594 if( is_wp_error( $error ) ) {595 $error_msg = sprintf( '(%s) %s', $error->get_error_code(), $error->get_error_message() );596 } else {597 $error_msg = $error;598 }599 600 if( class_exists( '\WC_Logger' ) ) {601 602 if( null === $this->logger ) {603 $this->logger = \wc_get_logger();604 }605 606 /**607 * The WC_Logger does not handle double quotes well.608 * This will conver double quotes to faux: " -> ''609 */610 array_walk_recursive( $context, function( &$val ) {611 $val = ( is_string( $val ) ) ? str_replace( '"', "''", $val ) : $val;612 } );613 614 $this->logger->log( $level, $error_msg, array_merge( $context, array( 'source' => 'live-rates-for-shipstation' ) ) );615 616 }617 618 return $error;619 620 }621 622 799 } -
live-rates-for-shipstation/trunk/core/api/shipstationv1.php
r3407166 r3452263 146 146 // Return Early - Something went wrong getting carriers. 147 147 } else if( ! isset( $carriers[ $carrier_code ] ) ) { 148 return $this->log( new \WP_Error( 404, esc_html__( '[v1] Could not find carrier information.', 'live-rates-for-shipstation' ) ) );148 return $this->log( new \WP_Error( 404, esc_html__( '[v1] Could not find carrier information.', 'live-rates-for-shipstation' ) ), 'warning' ); 149 149 } 150 150 … … 527 527 } ); 528 528 529 // Return Early - Skip the log but o529 // Return Early - No orders to work with. 530 530 if( empty( $orders ) ) { 531 return $this->log( new \WP_Error( 400, esc_html__( '[v1] Empty Orders. Data may be missing orderNumber or orderKey.', 'live-rates-for-shipstation' ) ), ' warning', array(531 return $this->log( new \WP_Error( 400, esc_html__( '[v1] Empty Orders. Data may be missing orderNumber or orderKey.', 'live-rates-for-shipstation' ) ), 'error', array( 532 532 'orders' => $order_arr, 533 533 ) ); … … 654 654 // Log API Request Result 655 655 /* translators: %s is the API endpoint (example: carriers/rates). */ 656 $this->log( sprintf( esc_html__( '[v1] ShipStation API Request to %s', 'live-rates-for-shipstation' ), $endpoint ), ' info', array(656 $this->log( sprintf( esc_html__( '[v1] ShipStation API Request to %s', 'live-rates-for-shipstation' ), $endpoint ), 'debug', array( 657 657 'args' => $args, 658 658 'code' => $code, 659 're ponse' => $body,659 'response' => $body, 660 660 ) ); 661 661 … … 692 692 */ 693 693 public function get_shipping_estimates( $est_opts ) { 694 return $this->log( new \WP_Error( 400, esc_html__( 'Live Rates for ShipStation v1 API Class does not support this endpoint. Use the v2 API Class: \IQLRSS\Core\Api\Shipstation', 'live-rates-for-shipstation' ) ) );694 return $this->log( new \WP_Error( 400, esc_html__( 'Live Rates for ShipStation v1 API Class does not support this endpoint. Use the v2 API Class: \IQLRSS\Core\Api\Shipstation', 'live-rates-for-shipstation' ) ), 'notice' ); 695 695 } 696 696 -
live-rates-for-shipstation/trunk/core/assets/js/admin.js
r3407166 r3452263 19 19 } ); 20 20 } 21 22 23 /** 24 * Edit Order Module Settings 25 * @import editOrderSettings 26 */ 27 if( document.querySelector( 'form#order' ) ) { 28 import( './edit-order/_main.js' ).then( ( Module ) => { 29 new Module.editOrderSettings(); 30 } ); 31 } -
live-rates-for-shipstation/trunk/core/assets/js/shipping-zones/_main.js
r3407166 r3452263 4 4 * Not really meant to be used as an object but more for 5 5 * encapsulation and organization. 6 * 7 * Manages the the possible initialization of custom boxes. 8 * Manages the show/hide functionality of price adjustments. 6 9 * 7 10 * @global {Object} iqlrss - Localized object of saved values. -
live-rates-for-shipstation/trunk/core/settings-shipstation.php
r3411187 r3452263 167 167 public function enqueue_admin_assets() { 168 168 169 global $wp_scripts;170 171 169 if( ! $this->maybe_enqueue( 'admin' ) ) { 172 170 return; … … 175 173 wp_enqueue_style( \IQLRSS\Driver::plugin_prefix( 'admin', '-' ) ); 176 174 wp_enqueue_script_module( \IQLRSS\Driver::plugin_prefix( 'admin', '-' ) ); 177 178 // if( current_user_can( 'list_users' ) ) {179 // $foo = wp_script_modules();180 // printf( '<pre>%s</pre>', print_r( $foo, 1 ) );181 // die( 'end' );182 // }183 175 184 176 } … … 211 203 212 204 global $wpdb; 205 213 206 214 207 /** … … 297 290 '' => esc_html__( 'ShipStation carriers may still be loading...', 'live-rates-for-shipstation' ), 298 291 ); 292 $warehouses = array( 293 '' => '(' . esc_html__( 'Website Store Address', 'live-rates-for-shipstation' ) . ')', 294 ); 299 295 $appended_fields = array(); 300 296 301 297 if( ! empty( \IQLRSS\Driver::get_ss_opt( 'api_key' ) ) ) { 302 298 303 $carrier_desc = esc_html__( 'Select which ShipStation carriers you would like to see live shipping rates from.', 'live-rates-for-shipstation' ); 304 $response = ( new Api\Shipstation() )->get_carriers(); 305 299 $api = new Api\Shipstation(); 300 301 // Grab Warehouse options 302 $api_warehouses = $api->get_warehouses(); 303 if( is_a( $api_warehouses, 'WP_Error' ) ) { 304 $warehouses = array( '' => $api_warehouses->get_error_message() ); 305 } else if( is_array( $api_warehouses ) && ! empty( $api_warehouses ) ) { 306 $warehouses = array_merge( $warehouses, array_combine( 307 array_keys( $api_warehouses ), 308 array_column( $api_warehouses, 'name' ), 309 ) ); 310 } 311 312 // Grab Carrier options 306 313 $carriers = array(); 307 if( is_a( $response, 'WP_Error' ) ) { 308 $carriers[''] = $response->get_error_message(); 309 } else if( is_array( $response ) ) { 310 foreach( $response as $carrier ) { 314 $api_carriers = $api->get_carriers(); 315 if( is_a( $api_carriers, 'WP_Error' ) ) { 316 $carriers[''] = $api_carriers->get_error_message(); 317 } else if( is_array( $api_carriers ) && ! empty( $api_carriers ) ) { 318 foreach( $api_carriers as $carrier ) { 311 319 $carriers[ $carrier['carrier_id'] ] = $carrier['name']; 312 320 } 313 321 } 314 322 315 } else {316 $carrier_desc = esc_html__( 'Please set and verify your ShipStation API key. Then, click the Save button at the bottom of this page.', 'live-rates-for-shipstation' );317 323 } 318 324 … … 340 346 'class' => 'chosen_select', 341 347 'options' => $carriers, 342 'description' => $carrier_desc, 348 'description' => ( function() { 349 if( ! empty( \IQLRSS\Driver::get_ss_opt( 'api_key' ) ) ) { 350 return esc_html__( 'Select which ShipStation carriers you would like to see live shipping rates from.', 'live-rates-for-shipstation' ); 351 } 352 return esc_html__( 'Please set and verify your ShipStation API key. Then, click the Save button at the bottom of this page.', 'live-rates-for-shipstation' ); 353 } )(), 343 354 'desc_tip' => esc_html__( 'Services from selected carriers will be available when setting up Shipping Zones.', 'live-rates-for-shipstation' ), 355 'default' => '', 356 ); 357 358 $appended_fields[ \IQLRSS\Driver::plugin_prefix( 'global_warehouse' ) ] = array( 359 'title' => esc_html__( 'Shipping From', 'live-rates-for-shipstation' ), 360 'type' => 'select', 361 'options' => $warehouses, 362 'description' => ( function() { 363 if( ! empty( \IQLRSS\Driver::get_ss_opt( 'api_key' ) ) ) { 364 return esc_html__( 'Select to ship from a different location than what is set as your WooCommerce website default location.', 'live-rates-for-shipstation' ); 365 } 366 return esc_html__( 'Please set and verify your ShipStation API key. Then, click the Save button at the bottom of this page.', 'live-rates-for-shipstation' ); 367 } )(), 368 'desc_tip' => esc_html__( 'This can be overridden per Shipping Zone.', 'live-rates-for-shipstation' ), 344 369 'default' => '', 345 370 ); … … 503 528 $enqueue = ( $enqueue || ( isset( $_GET, $_GET['section'] ) && 'shipstation' == $_GET['section'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 504 529 530 // Edit Order page 531 $enqueue = ( $enqueue || ( isset( $_GET, $_GET['page'], $_GET['id'] ) && 'wc-orders' == $_GET['page'] && ! empty( $_GET['id'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 532 505 533 // Overprotective WooCommerce settings page check 506 $enqueue = ( $enqueue && 'woocommerce_page_wc-settings' == $screen_id);534 $enqueue = ( $enqueue && in_array( $screen_id, array( 'woocommerce_page_wc-orders', 'woocommerce_page_wc-settings' ) ) ); 507 535 } 508 536 return $enqueue; -
live-rates-for-shipstation/trunk/core/shipping-method-shipstation.php
r3442676 r3452263 2 2 /** 3 3 * ShipStation Live Shipping Rates Method 4 *5 * @todo Consider moving Shipping Calculations into it's own class.6 4 * 7 5 * @link https://www.fedex.com/en-us/shipping/one-rate.html … … 16 14 */ 17 15 namespace IQLRSS\Core; 16 use \IQLRSS\Core\Traits; 18 17 19 18 if( ! defined( 'ABSPATH' ) ) { … … 26 25 27 26 /** 27 * Inherit logger traits 28 */ 29 use Traits\Logger; 30 31 32 /** 28 33 * Plugin prefix used to namespace data keys. 29 34 * … … 31 36 */ 32 37 protected $plugin_prefix; 33 34 35 /**36 * Array of store specific settings.37 *38 * @var Array39 */40 protected $store_data = array(41 'weight_unit' => '',42 'dim_unit' => '', // Dimension43 );44 38 45 39 … … 94 88 } 95 89 96 // Set the store unit term and associate it with ShipStations term.97 $this->store_data = array(98 'weight_unit' => get_option( 'woocommerce_weight_unit', $this->store_data['weight_unit'] ),99 'dim_unit' => get_option( 'woocommerce_dimension_unit', $this->store_data['dim_unit'] ),100 );101 102 90 /** 103 91 * Init shipping methods. … … 189 177 /** 190 178 * Increase the HTTP Request Timeout 191 * Sometimes ShipStation takes awhile to respond ewith rates.179 * Sometimes ShipStation takes awhile to respond with rates. 192 180 * Presumably, the more services enabled, the longer it takes. 193 181 * … … 409 397 protected function init_instance_form_fields() { 410 398 399 $store_warehouse_label = '(' . esc_html__( 'Website Store Address', 'live-rates-for-shipstation' ) . ')'; 400 $warehouses = array( '' => $store_warehouse_label ); 401 402 if( ! empty( \IQLRSS\Driver::get_ss_opt( 'api_key' ) ) ) { 403 404 $og_warehouse = \IQLRSS\Driver::get_ss_opt( 'global_warehouse' ); 405 $api = new Api\Shipstation(); 406 407 // Grab Warehouse options 408 $api_warehouses = $api->get_warehouses(); 409 if( is_a( $api_warehouses, 'WP_Error' ) ) { 410 $warehouses = array( '' => $api_warehouses->get_error_message() ); 411 } else if( is_array( $api_warehouses ) && ! empty( $api_warehouses ) ) { 412 $warehouses = array_merge( $warehouses, array_combine( 413 array_keys( $api_warehouses ), 414 array_column( $api_warehouses, 'name' ), 415 ) ); 416 } 417 418 // Move the global warehouse to the top. 419 if( ! empty( $og_warehouse ) && isset( $warehouses[ $og_warehouse ] ) ) { 420 $tmp_houses = array_diff_key( $warehouses, array( '' => '' ) ); 421 $warehouses = array_merge( array( 422 '' => $warehouses[ $og_warehouse ] . ' (' . esc_html__( 'Store Global', 'live-rates-for-shipstation' ) . ')', 423 '_woo_default' => $store_warehouse_label, 424 ), $tmp_houses ); 425 } 426 } 427 411 428 $settings = array( 412 429 'title' => array( … … 416 433 'default' => esc_html__( 'ShipStation Rates', 'live-rates-for-shipstation' ), 417 434 'desc_tip' => true, 435 ), 436 'warehouse' => array( 437 'title' => esc_html__( 'Shipping Location', 'live-rates-for-shipstation' ), 438 'type' => 'select', 439 'options' => $warehouses, 440 'description' => esc_html__( 'Select to ship from a different location than the default.', 'live-rates-for-shipstation' ), 418 441 ), 419 442 'minweight' => array( … … 453 476 /** 454 477 * Allow filtering the Shipping Zone settings 455 * 478 * 456 479 * @hook filter 457 * 480 * 458 481 * @param Array $settings 459 482 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 460 * 483 * 461 484 * @return Array $settings 462 485 */ … … 481 504 482 505 ob_start(); 483 include 'assets/views/customboxes-table.php';506 include \IQLRSS\Driver::get_asset_path( 'views/shipping-zone/customboxes-table.php' ); 484 507 return ob_get_clean(); 485 508 … … 590 613 591 614 ob_start(); 592 include 'assets/views/services-table.php';615 include \IQLRSS\Driver::get_asset_path( 'views/shipping-zone/services-table.php' ); 593 616 return ob_get_clean(); 594 617 … … 660 683 } 661 684 685 662 686 /** 663 687 * We don't want to array_filter() since … … 703 727 } 704 728 705 $enabled_services = $this->get_enabled_services(); 706 if( empty( $enabled_services ) ) { 707 $this->log( esc_html__( 'No enabled carrier services found. Please enable carrier services within the shipping zone.', 'live-rates-for-shipstation' ) ); 708 return; 709 } 710 711 $saved_carriers = array_keys( $enabled_services ); 712 if( ! empty( $saved_carriers ) && ! empty( $this->carriers ) ) { 713 $saved_carriers = array_values( array_intersect( $saved_carriers, $this->carriers ) ); 714 } 715 716 $global_adjustment = floatval( \IQLRSS\Driver::get_ss_opt( 'global_adjustment', 0 ) ); 717 $global_adjustment_type = \IQLRSS\Driver::get_ss_opt( 'global_adjustment_type','' ); 718 $global_adjustment_type = ( empty( $global_adjustment_type ) && ! empty( $global_adjustment ) ) ? 'percentage' : $global_adjustment_type; 719 720 $packing_type = $this->get_option( 'packing', 'individual' ); 721 $request = array( 722 'from_country_code' => WC()->countries->get_base_country(), 723 'from_postal_code' => WC()->countries->get_base_postcode(), 724 'from_city_locality' => WC()->countries->get_base_city(), 725 'from_state_province'=> WC()->countries->get_base_state(), 726 727 'to_country_code' => $packages['destination']['country'], 728 'to_postal_code' => $packages['destination']['postcode'], 729 'to_city_locality' => $packages['destination']['city'], 730 'to_state_province' => $packages['destination']['state'], 731 732 'address_residential_indicator' => 'unknown', 733 ); 734 735 $item_requests = array(); 736 $callback = sprintf( 'group_requestsby_%s', str_replace( '-', '_', $packing_type ) ); 737 if( method_exists( $this, $callback ) ) { 738 $item_requests = call_user_func( array( $this, $callback ), $packages['contents'] ); 739 } 729 // Grab the calculator to be filtered. 730 $calculator = new Classes\Shipping_Calculator( $packages, array( 731 'shipping_method' => $this, 732 ) ); 740 733 741 734 742 735 /** 743 * Allow filtering the packages before requesting estimates. 744 * 745 * The returned array should follow this format: 746 * Multi-dimensional Array 747 * 748 * $item_requests = Array( Array( 749 * ~ Required Fields: 750 * '_name' => '$productID|$productName', - This format makes it easy to show the Shop Manager what's packed into the box. 751 * 'dimensions' => array( 752 * 'length => 123, 753 * 'width' => 123, 754 * 'height' => 123, 755 * 'unit' => 'inch', - ShipStation expects a specific string. See \IQLRSS\Core\Api\Shipstation::convert_unit_term( $unit ) 756 * ), 757 * 'weight' => array( 758 * 'value' => 123, 759 * 'unit' => 'pound', - ShipStation expects a specific string. See \IQLRSS\Core\Api\Shipstation::convert_unit_term( $unit ) 760 * ), 761 * 762 * ~ Entirely optional, but the system will try to read them if available. 763 * 'packed' => Array( '$productID|$productName', '$productID|$productName' ), 764 * 'price' => 123, 765 * 'nickname' => 'String' - Displayed to the Shop Owner on the Edit Order page. 766 * 'box_weight' => 123, 767 * 'box_max_weight'=> 123, 768 * 'package_code' => 'ups_ground', 769 * 'carrier_code' => 'ups', - Carrier Code should match what ShipStation expects. I.E. fedex_walleted. This is to group packages with carriers for discounts. 770 * ) ) 771 * 736 * Allow overriding the Shipping Calculator object. 737 * Must inherit IQLRSS\Core\Classes\Shipping_Calculator 738 * 772 739 * @hook filter 773 * 774 * @param Array $item_requests - Array of Package dimensions that the API will use to get rates on. Multidimensional Array.775 * @param Array $packages - The cart contents. See $packages['contents'] for items. 740 * 741 * @param \IQLRSS\Core\Classes\Shipping_Calculator $calculator 742 * @param Array $packages - The cart contents. See $packages['contents'] for items. 776 743 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 777 * 744 * 778 745 * @return Array $settings 779 746 */ 780 $filtered_requests = apply_filters( 'iqlrss/shipping/packages', $item_requests, $packages, $packing_type, $this ); 781 782 // IF the hash doesn't match what was given to the filter, note it in the logs so the store owner will know. 783 $item_req_hash = ( ! empty( $item_requests ) ) ? md5( maybe_serialize( $item_requests ) ) : ''; 784 $filtered_req_hash = ( ! empty( $filtered_requests ) ) ? md5( maybe_serialize( $filtered_requests ) ) : ''; 785 if( $item_req_hash !== $filtered_req_hash ) { 786 $this->log( esc_html__( 'The Shipping packages were modified by a 3rd party using the `iqlrss/shipping/packages` filter hook.', 'live-rates-for-shipstation' ), 'notice' ); 787 } 788 789 /** 790 * We have to return reates per package. 791 * The /rates/estimate endpoint requires less info 792 * and /rates endpoint is way slower. 793 */ 794 $rates = array(); 795 foreach( $filtered_requests as $item_id => $req ) { 796 797 // Create the API request combining the package (weight, dimensions), general request data, and the carrier info. 798 $api_request = array_merge( 799 $req, // Package (weight, dimensions) 800 $request, // General info like to/from address 801 array( // Saved carrier ids 802 'carrier_ids' => $saved_carriers, 803 ) 804 ); 805 806 // Ping the ShipStation API to get rates per Carrier. 807 // Continue - Something went wrong, should be logged on the API side. 808 $available_rates = $this->shipStationApi->get_shipping_estimates( $api_request ); 809 810 if( is_wp_error( $available_rates ) || empty( $available_rates ) ) { 811 continue; 747 $maybe_calc = apply_filters( 'iqlrss/shipping/calculator_object', $calculator, $packages, $this ); 748 if( is_object( $maybe_calc ) && $maybe_calc !== $calculator ) { 749 750 // Override calculator object and log it's change. 751 if( is_subclass_of( $maybe_calc, '\IQLRSS\Core\Classes\Shipping_Calculator' ) ) { 752 753 $calculator = $maybe_calc; 754 $this->log( sprintf( '%s [%s]', 755 esc_html__( 'Shipping Calculations Object overridden.', 'live-rates-for-shipstation' ), 756 get_class( $maybe_calc ) 757 ), 'notice' ); 758 759 // Something went wrong, log that too. 760 } else { 761 762 $this->log( sprintf( '%s [%s]', 763 esc_html__( 'Shipping Calculations Object override failed. Class may not inherit "\IQLRSS\Core\Classes\Shipping_Calculator".', 'live-rates-for-shipstation' ), 764 get_class( $maybe_calc ), 765 'warning' 766 ) ); 767 812 768 } 813 814 // Loop the found rates and setup the WooCommerce rates array for each. 815 foreach( $available_rates as $shiprate ) { 816 817 if( ! isset( $enabled_services[ $shiprate['carrier_id'] ][ $shiprate['code'] ] ) ) { 818 continue; 819 } 820 821 $ratehash = md5( sprintf( '%s%s', $shiprate['code'], $shiprate['carrier_id'] ) ); 822 $service_arr = $enabled_services[ $shiprate['carrier_id'] ][ $shiprate['code'] ]; 823 $cost = floatval( $shiprate['cost'] ); 824 $rate_name = ( isset( $req['_name'] ) ) ? $req['_name'] : ''; 825 $rate_name = ( empty( $rate_name ) && isset( $req['nickname'] ) ) ? $req['nickname'] : $rate_name; 826 $ratemeta = array( 827 '_name'=> $rate_name, // Item products(ID|Name) or box nickname. 828 'rate' => $cost, 829 ); 830 831 // Apply service upcharge 832 if( isset( $service_arr['adjustment'] ) ) { 833 834 /** 835 * Adjustment type could be '' to skip global adjustment. 836 * Defaults to percentage for v1.03 backwards compatibility. 837 */ 838 $adjustment = floatval( $service_arr['adjustment'] ); 839 $adjustment_type = ( isset( $service_arr['adjustment_type'] ) ) ? $service_arr['adjustment_type'] : 'percentage'; 840 841 if( ! empty( $adjustment_type ) && $adjustment > 0 ) { 842 843 $adjustment_cost = ( 'percentage' == $adjustment_type ) ? ( $cost * ( floatval( $adjustment ) / 100 ) ) : floatval( $adjustment ); 844 $ratemeta['adjustment'] = array( 845 'type' => $adjustment_type, 846 'rate' => $adjustment, 847 'cost' => $adjustment_cost, 848 'global'=> false, 849 ); 850 $cost += $adjustment_cost; 851 852 } 853 854 } else if( ! empty( $global_adjustment_type ) && $global_adjustment > 0 ) { 855 856 $adjustment_cost = ( 'percentage' == $global_adjustment_type ) ? ( $cost * ( floatval( $global_adjustment ) / 100 ) ) : floatval( $global_adjustment ); 857 $ratemeta['adjustment'] = array( 858 'type' => $global_adjustment_type, 859 'rate' => $global_adjustment, 860 'cost' => $adjustment_cost, 861 'global'=> true, 862 ); 863 $cost += $adjustment_cost; 864 865 } 866 867 // Loop and add any other shipment amounts. 868 if( ! empty( $shiprate['other_costs'] ) ) { 869 870 $ratemeta['other_costs'] = array(); 871 foreach( $shiprate['other_costs'] as $slug => $cost_arr ) { 872 873 if( empty( $cost_arr['amount'] ) ) continue; 874 $cost += floatval( $cost_arr['amount'] ); 875 $ratemeta['other_costs'][ $slug ] = $cost_arr['amount']; 876 877 } 878 } 879 880 // Maybe a package price 881 if( 'wc-box-packer' == $packing_type && isset( $req['price'] ) && ! empty( $req['price'] ) ) { 882 $cost += floatval( $req['price'] ); 883 $ratemeta['other_costs']['box_price'] = $req['price']; 884 } 885 886 // Maybe apply per item. 887 if( 'individual' == $packing_type ) { 888 $cost *= $packages['contents'][ $item_id ]['quantity']; 889 $ratemeta['qty'] = $packages['contents'][ $item_id ]['quantity']; 890 } 891 892 // Set rate or append the estimated item ship cost. 893 if( ! isset( $rates[ $ratehash ] ) ) { 894 895 $rates[ $ratehash ] = array( 896 'id' => $ratehash, 897 'label' => ( ! empty( $service_arr['nickname'] ) ) ? $service_arr['nickname'] : $shiprate['name'], 898 'package' => $packages, 899 'cost' => array( $cost ), 900 'meta_data' => array( 901 'carrier' => $shiprate['carrier_name'], 902 'service' => $shiprate['name'], 903 'rates' => array(), 904 'boxes' => array(), 905 906 // Private metadata fields must be excluded via filter way above. 907 "_{$this->plugin_prefix}_carrier_id" => $shiprate['carrier_id'], 908 "_{$this->plugin_prefix}_carrier_code" => $shiprate['carrier_code'], 909 "_{$this->plugin_prefix}_service_code" => $shiprate['code'], 910 ), 911 ); 912 913 } else { 914 $rates[ $ratehash ]['cost'][] = $cost; 915 } 916 917 // Merge item rates 918 $rates[ $ratehash ]['meta_data']['rates'] = array_merge( 919 $rates[ $ratehash ]['meta_data']['rates'], 920 array( $ratemeta ), 921 ); 922 923 // Merge item boxes 924 $rates[ $ratehash ]['meta_data']['boxes'] = array_merge( 925 $rates[ $ratehash ]['meta_data']['boxes'], 926 array( $req ), 927 ); 928 769 } 770 771 // Get and Add Rates - EZPZ 772 if( $rates = $calculator->get_rates() ) { 773 foreach( $rates as $rate ) { 774 $this->add_rate( $rate ); 929 775 } 930 931 }932 933 $single_lowest = \IQLRSS\Driver::get_ss_opt( 'return_lowest', 'no' );934 $single_lowest_label = \IQLRSS\Driver::get_ss_opt( 'return_lowest_label', '' );935 936 // Add all shipping rates, let the user decide.937 if( 'no' == $single_lowest || empty( $single_lowest ) ) {938 939 foreach( $rates as $rate_arr ) {940 941 // If more than 1 rate, add the cheapest.942 if( count( $rate_arr['cost'] ) > 1 ) {943 usort( $rate_arr['cost'], fn( $r1, $r2 ) => ( (float)$r1 < (float)$r2 ) ? -1 : 1 );944 $rate_arr['cost'] = (array)array_shift( $rate_arr['cost'] );945 }946 947 // WooCommerce skips serialized data when outputting order item meta, this is a workaround.948 // See hooks above for formatting.949 $rate_arr['meta_data']['rates'] = wp_json_encode( $rate_arr['meta_data']['rates'] );950 $rate_arr['meta_data']['boxes'] = wp_json_encode( $rate_arr['meta_data']['boxes'] );951 952 $this->add_rate( $rate_arr );953 }954 955 // Find the single lowest shipping rate956 } else if( 'yes' == $single_lowest ) {957 958 $lowest = 0;959 $lowest_service = array_key_first( $rates );960 foreach( $rates as $service_id => $rate_arr ) {961 962 $total = array_sum( $rate_arr['cost'] );963 if( 0 == $lowest || $total < $lowest ) {964 $lowest = $total;965 $lowest_service = $service_id;966 }967 }968 969 if( ! empty( $single_lowest_label ) ) {970 $rates[ $lowest_service ]['label'] = $single_lowest_label;971 }972 973 // WooCommerce skips serialized data when outputting order item meta, this is a workaround.974 // See hooks above for formatting.975 $rates[ $lowest_service ]['meta_data']['rates'] = wp_json_encode( $rates[ $lowest_service ]['meta_data']['rates'] );976 $rates[ $lowest_service ]['meta_data']['boxes'] = wp_json_encode( $rates[ $lowest_service ]['meta_data']['boxes'] );977 978 $this->add_rate( $rates[ $lowest_service ] );979 980 776 } 981 777 … … 994 790 995 791 /** 996 * Return an array of API requests which would be for individual products.997 *998 * @param Array $items999 *1000 * @return Array $requests1001 */1002 public function group_requestsby_individual( $items ) {1003 1004 $item_requests = array();1005 $default_weight = $this->get_option( 'minweight', '' );1006 1007 foreach( $items as $item_id => $item ) {1008 1009 // Continue - No shipping needed for product.1010 if( ! $item['data']->needs_shipping() ) {1011 continue;1012 }1013 1014 $request = array(1015 '_name' => sprintf( '%s|%s',1016 $item['data']->get_id(),1017 $item['data']->get_name(),1018 ),1019 'weight' => ( ! empty( $item['data']->get_weight() ) ) ? $item['data']->get_weight() : $default_weight,1020 );1021 $physicals = array_filter( array(1022 'length' => $item['data']->get_length(),1023 'width' => $item['data']->get_width(),1024 'height' => $item['data']->get_height(),1025 ) );1026 1027 // Return Early - Product missing one of the 4 key dimensions.1028 if( count( $physicals ) < 3 || empty( $request['weight'] ) ) {1029 $this->log( sprintf(1030 1031 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */1032 esc_html__( 'Product ID #%1$d missing (%2$s) dimensions. Weight is a minimum requirement. Shipping calculations terminated.', 'live-rates-for-shipstation' ),1033 $item['product_id'],1034 implode( ', ', array_diff_key( array(1035 'length' => 'length',1036 'width' => 'width',1037 'height' => 'height',1038 'weight' => 'weight',1039 ), $physicals + array( 'weight' => $request['weight'] ) ) )1040 ) );1041 1042 return array();1043 }1044 1045 // Set rate request dimensions.1046 sort( $physicals );1047 if( 3 == count( $physicals ) ) {1048 $request['dimensions'] = array(1049 'length' => round( wc_get_dimension( $physicals[2], $this->store_data['dim_unit'] ), 2 ),1050 'width' => round( wc_get_dimension( $physicals[1], $this->store_data['dim_unit'] ), 2 ),1051 'height' => round( wc_get_dimension( $physicals[0], $this->store_data['dim_unit'] ), 2 ),1052 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ),1053 );1054 }1055 1056 // Set rate request weight.1057 if( ! empty( $request['weight'] ) ) {1058 $request['weight'] = array(1059 'value' => (float)round( wc_get_weight( $request['weight'], $this->store_data['weight_unit'] ), 2 ),1060 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),1061 );1062 }1063 1064 $item_requests[ $item_id ] = $request;1065 1066 }1067 1068 return $item_requests;1069 1070 }1071 1072 1073 /**1074 * One Big Box1075 * Group all the products by weight and get rates by total weight.1076 *1077 * @param Array $items1078 *1079 * @return Array $requests1080 */1081 public function group_requestsby_onebox( $items ) {1082 1083 $default_weight = $this->get_option( 'minweight', 0 );1084 $subtype = $this->get_option( 'packing_sub', 'weightonly' );1085 $dimensions = array(1086 'running' => array_combine( array( 'length', 'width', 'height', 'weight' ), array_fill( 0, 4, 0 ) ),1087 'largest' => array_combine( array( 'length', 'width', 'height', 'weight' ), array_fill( 0, 4, 0 ) ),1088 );1089 1090 foreach( $items as $item_id => $item ) {1091 1092 // Continue - No shipping needed for product.1093 if( ! $item['data']->needs_shipping() ) {1094 continue;1095 }1096 1097 $request = array(1098 '_name' => sprintf( '%s|%s',1099 $item['data']->get_id(),1100 $item['data']->get_name(),1101 ),1102 'weight' => ( ! empty( $item['data']->get_weight() ) ) ? $item['data']->get_weight() : $default_weight,1103 );1104 1105 // Return Early - Missing minimum requirement: weight.1106 if( empty( $request['weight'] ) ) {1107 1108 $this->log( sprintf(1109 1110 /* translators: %1$d is the Product ID. */1111 esc_html__( 'Product ID #%1$d missing weight. Shipping Zone weight fallback could not be used. Shipping calculations terminated.', 'live-rates-for-shipstation' ),1112 $item['product_id']1113 ) );1114 1115 return array();1116 1117 }1118 1119 $dimensions['running']['weight'] = $dimensions['running']['weight'] + ( floatval( $request['weight'] ) * $item['quantity'] );1120 $dimensions['running']['height'] = $dimensions['running']['height'] + ( floatval( $item['data']->get_height() ) * $item['quantity'] );1121 $dimensions['largest'] = array(1122 'length' => ( $dimensions['largest']['length'] < $item['data']->get_length() ) ? $item['data']->get_length() : $dimensions['largest']['length'],1123 'width' => ( $dimensions['largest']['width'] < $item['data']->get_width() ) ? $item['data']->get_width() : $dimensions['largest']['width'],1124 'height' => ( $dimensions['largest']['height'] < $item['data']->get_height() ) ? $item['data']->get_height() : $dimensions['largest']['height'],1125 'weight' => ( $dimensions['largest']['weight'] < $request['weight'] ) ? $request['weight'] : $dimensions['largest']['weight'],1126 );1127 1128 }1129 1130 // Return Early - Rates by total weight.1131 if( 'weightonly' == $subtype ) {1132 1133 return array( array(1134 'weight' => array(1135 'value' => (float)round( wc_get_weight( $dimensions['running']['weight'], $this->store_data['weight_unit'] ), 2 ),1136 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),1137 ),1138 ) );1139 1140 }1141 1142 $physicals = array_filter( array(1143 'length' => $dimensions['largest']['length'],1144 'width' => $dimensions['largest']['width'],1145 'height' => $dimensions['running']['height'],1146 'weight' => $dimensions['running']['weight'],1147 ) );1148 1149 // Return Early - Error - Missing dimensions to work with.1150 if( $physicals < 4 ) {1151 1152 $this->log( sprintf(1153 1154 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */1155 esc_html__( 'OneBox rate requestion missing dimensions (%1$s). Weight is a minimum requirement. Shipping calculations terminated.', 'live-rates-for-shipstation' ),1156 implode( ', ', array_diff_key( array(1157 'length' => 'length',1158 'width' => 'width',1159 'height' => 'height',1160 'weight' => 'weight',1161 ), $physicals ) )1162 ) );1163 1164 return array();1165 1166 }1167 1168 // Default - Stacked Verticially1169 return array( array(1170 'weight' => array(1171 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),1172 'value' => (float)round( wc_get_weight( $physicals['weight'], $this->store_data['weight_unit'] ), 2 ),1173 ),1174 'dimensions' => array(1175 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ),1176 1177 // Largest1178 'length' => round( wc_get_dimension( $physicals['length'], $this->store_data['dim_unit'] ), 2 ),1179 'width' => round( wc_get_dimension( $physicals['width'], $this->store_data['dim_unit'] ), 2 ),1180 1181 // Running1182 'height' => round( wc_get_dimension( $physicals['height'], $this->store_data['dim_unit'] ), 2 ),1183 ),1184 ) );1185 1186 }1187 1188 1189 /**1190 * Return an array of API requests for custom packed boxes.1191 * Shoutout to Mike Jolly & Co.1192 *1193 * @param Array $items1194 *1195 * @return Array $requests1196 */1197 public function group_requestsby_wc_box_packer( $items ) {1198 1199 $item_requests = array();1200 $boxes = $this->get_option( 'customboxes', array() );1201 $default_weight = $this->get_option( 'minweight', '' );1202 1203 /* Return Early - No custom boxes found. */1204 if( empty( $boxes ) ) {1205 $this->log( esc_html__( 'Custom Boxes selected, but no boxes found. Items packed individually', 'live-rates-for-shipstation' ), 'warning' );1206 return $this->group_requestsby_individual( $items );1207 }1208 1209 if( ! class_exists( '\IQRLSS\WC_Box_Packer\WC_Boxpack' ) ) {1210 include_once 'wc-box-packer/class-wc-boxpack.php';1211 }1212 1213 // Setup the WC_Boxpack boxes based on user submitted custom boxes.1214 $wc_boxpack = new WC_Box_Packer\WC_Boxpack();1215 foreach( $boxes as $box ) {1216 if( empty( $box['active'] ) ) continue;1217 $wc_boxpack->add_box( $box );1218 }1219 1220 // Loop the items, grabs their dimensions, and assocaite them with WC_Boxpack for future packing.1221 foreach( $items as $item_id => $item ) {1222 if( ! $item['data']->needs_shipping() ) continue;1223 1224 $weight = ( ! empty( $item['data']->get_weight() ) ) ? $item['data']->get_weight() : $default_weight;1225 $data = array(1226 'weight' => (float)round( wc_get_weight( $weight, $this->store_data['weight_unit'] ), 2 ),1227 );1228 $physicals = array_filter( array(1229 'length' => $item['data']->get_length(),1230 'width' => $item['data']->get_width(),1231 'height' => $item['data']->get_height(),1232 ) );1233 1234 // Return Early - Product missing one of the 4 key dimensions.1235 if( count( $physicals ) < 3 && empty( $data['weight'] ) ) {1236 $this->log( sprintf(1237 1238 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */1239 esc_html__( 'Product ID #%1$d missing (%2$s) dimensions and no weight found. Shipping calculations terminated.', 'live-rates-for-shipstation' ),1240 $item['product_id'],1241 implode( ', ', array_diff_key( array(1242 'width' => 'width',1243 'height' => 'height',1244 'length' => 'length',1245 ), $physicals ) )1246 ) );1247 return array();1248 }1249 1250 sort( $physicals );1251 $data = array(1252 'length' => round( wc_get_dimension( $physicals[2], $this->store_data['dim_unit'] ), 2 ),1253 'width' => round( wc_get_dimension( $physicals[1], $this->store_data['dim_unit'] ), 2 ),1254 'height' => round( wc_get_dimension( $physicals[0], $this->store_data['dim_unit'] ), 2 ),1255 'weight' => round( wc_get_weight( $data['weight'], $this->store_data['weight_unit'] ), 2 ),1256 );1257 1258 // Pack Products1259 for( $i = 0; $i < $item['quantity']; $i++ ) {1260 $wc_boxpack->add_item(1261 $data['length'],1262 $data['width'],1263 $data['height'],1264 $data['weight'],1265 $item['data']->get_price(),1266 array(1267 '_name' => sprintf( '%s|%s',1268 $item['data']->get_id(),1269 $item['data']->get_name(),1270 ),1271 ),1272 );1273 }1274 }1275 1276 // Pack it up, missions over.1277 $wc_boxpack->pack();1278 $wc_box_packages = $wc_boxpack->get_packages();1279 $box_log = array();1280 1281 // Delivery!1282 foreach( $wc_box_packages as $key => $package ) {1283 1284 $packed_items = ( is_array( $package->packed ) ) ? array_map( function( $item ) { return $item->meta['_name']; }, $package->packed ) : array();1285 $item_requests[] = array(1286 'weight' => array(1287 'value' => round( $package->weight, 2 ),1288 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),1289 ),1290 'dimensions' => array(1291 'length' => round( $package->length, 2 ),1292 'width' => round( $package->width, 2 ),1293 'height' => round( $package->height, 2 ),1294 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ),1295 ),1296 'packed' => $packed_items,1297 'price' => ( ! empty( $package->data ) ) ? $package->data['price'] : 0,1298 'nickname' => ( ! empty( $package->data ) ) ? $package->data['nickname'] : '',1299 'box_weight' => ( ! empty( $package->data ) ) ? $package->data['weight'] : 0,1300 'box_max_weight'=> ( ! empty( $package->data ) ) ? $package->data['weight_max'] : 0,1301 'package_code' => ( ! empty( $package->data ) ) ? $package->data['preset'] : '',1302 'carrier_code' => ( ! empty( $package->data ) ) ? $package->data['carrier_code'] : '',1303 );1304 1305 $box_log[] = array(1306 'is_packed' => boolval( empty( $package->unpacked ) ),1307 'item_count' => count( $package->packed ),1308 'items' => $packed_items,1309 'box_dimensions' => sprintf( '%s x %s x %s | %s | %s', $package->length, $package->width, $package->height, $package->weight, $package->volume ),1310 'box_dim_key' => sprintf( '%s x %s x %s | %s | %s',1311 esc_html__( 'Length', 'live-rates-for-shipstation' ),1312 esc_html__( 'Width', 'live-rates-for-shipstation' ),1313 esc_html__( 'Height', 'live-rates-for-shipstation' ),1314 esc_html__( 'Weight', 'live-rates-for-shipstation' ),1315 esc_html__( 'Volume', 'live-rates-for-shipstation' ),1316 ),1317 'max_volume' => floatval( $package->width * $package->height * $package->length ),1318 'data' => ( ! empty( $package->data ) ) ? $package->data : array(),1319 );1320 1321 }1322 1323 if( ! empty( $box_log ) ) {1324 $this->log( esc_html__( 'Custom Boxes Packed', 'live-rates-for-shipstation' ), 'info', $box_log );1325 }1326 1327 return $item_requests;1328 1329 }1330 1331 1332 /**1333 792 * Set the rates based on cached packages. 1334 793 * … … 1343 802 protected function check_packages_rate_cache( $packages ) { 1344 803 804 805 /** 806 * Maybe skip cart caches. 807 * Do note that WooCommerce makes multiple calls to the cart / calculations. 808 * Disabling this may result in many more API calls than expected. 809 * 810 * @hook filter 811 * 812 * @param Bolean TRUE 813 * 814 * @return Boolean 815 */ 816 // Return Early - Filter Skips Cache. 817 if( true !== apply_filters( 'iqlrss/cache/cart_rates', true ) ) return; 818 1345 819 $session = WC()->session->get( $this->plugin_prefix . '_packages', array() ); 1346 820 $cleartime = get_transient( \IQLRSS\Driver::plugin_prefix( 'wcs_timeout' ) ); … … 1359 833 $size = count( $packages ); 1360 834 for( $i = 0; $i < $size; $i++ ) { 1361 1362 835 $cache = WC()->session->get( 'shipping_for_package_' . $i, false ); 1363 if( empty( $cache ) || ! is_array( $cache ) ) { 1364 break; 1365 } 836 if( empty( $cache ) || ! is_array( $cache ) ) break; 1366 837 $this->rates = array_merge( $cache['rates'], $this->rates ); 1367 1368 838 } 1369 839 … … 1469 939 1470 940 /** 1471 * Return an m-array of enabled services grouped by carrier key.1472 *1473 * @return Array1474 */1475 public function get_enabled_services() {1476 1477 $enabled = array();1478 $saved_services = $this->get_option( 'services', array() );1479 if( empty( $saved_services ) ) return $enabled;1480 1481 foreach( $saved_services as $c => $sa ) {1482 foreach( $sa as $sk => $s ) {1483 if( ! isset( $s['enabled'] ) || ! $s['enabled'] ) continue;1484 $enabled[ $c ][ $sk ] = $s;1485 }1486 }1487 1488 return $enabled;1489 1490 }1491 1492 1493 /**1494 941 * Convert a WooCommerce unit to a ShipStation unit. 1495 * 942 * 1496 943 * @param String $unit 1497 * 944 * 1498 945 * @return String $new_unit 1499 946 */ … … 1515 962 } 1516 963 1517 $global_carriers= $this->shipStationApi->get_carriers(); 1518 $carrier_codes = wp_list_pluck( $global_carriers, 'carrier_code' ); 1519 $carrier_codes = array_intersect_key( $carrier_codes, array_flip( $this->carriers ) ); 964 $global_carriers = $this->shipStationApi->get_carriers(); 965 $carrier_codes = wp_list_pluck( $global_carriers, 'carrier_code' ); 966 $carrier_codes = array_intersect_key( $carrier_codes, array_flip( $this->carriers ) ); 967 $carrier_packages = array(); 1520 968 1521 969 $data = array( … … 1534 982 ); 1535 983 984 // Append ShipStation Packages 985 $sspackages = $this->shipStationApi->get_packages(); 986 if( ! is_wp_error( $sspackages ) && ! empty( $sspackages ) ) { 987 988 $carrier_packages['shipstation'] = array( 989 'label' => esc_html__( 'ShipStation' ), 990 'packages' => array(), 991 ); 992 993 foreach( $sspackages as $package ) { 994 $carrier_packages['shipstation']['packages'][] = array( 995 'label' => $package['name'], 996 'code' => $package['package_id'], 997 'length' => $package['dimensions']['length'], 998 'width' => $package['dimensions']['width'], 999 'height' => $package['dimensions']['height'], 1000 'weight_max' => '', 1001 'carrier_code' => '', 1002 ); 1003 } 1004 } 1005 1536 1006 // Append Translated Labels 1537 $carrier_packages = array();1538 1007 foreach( $data as $carrier_code => &$carriers ) { 1539 1008 … … 1640 1109 } 1641 1110 1642 1643 /**1644 * Log error in WooCommerce1645 * Passthru method - log what's given and give it back.1646 * Could make a good Trait1647 *1648 * @param Mixed $error - String or WP_Error1649 * @param String $level - WooCommerce level (debug|info|notice|warning|error|critical|alert|emergency)1650 * @param Array $context1651 *1652 * @return Mixed - Return the error back.1653 */1654 protected function log( $error, $level = 'debug', $context = array() ) {1655 1656 if( ! \IQLRSS\Driver::get_ss_opt( 'logging_enabled', 0, true ) ) {1657 return $error;1658 }1659 1660 if( is_wp_error( $error ) ) {1661 $error_msg = sprintf( '(%s) %s', $error->get_error_code(), $error->get_error_message() );1662 } else {1663 $error_msg = $error;1664 }1665 1666 if( class_exists( '\WC_Logger' ) ) {1667 1668 if( null === $this->logger ) {1669 $this->logger = \wc_get_logger();1670 }1671 1672 $this->logger->log( $level, $error_msg, array_merge( $context, array( 'source' => 'live-rates-for-shipstation' ) ) );1673 1674 }1675 1676 return $error;1677 1678 }1679 1680 1111 } -
live-rates-for-shipstation/trunk/core/traits/logger.php
r3442676 r3452263 22 22 * @return Mixed - Return the error back. 23 23 */ 24 protected function log( $error, $level = ' debug', $context = array() ) {24 protected function log( $error, $level = 'info', $context = array() ) { 25 25 26 26 if( ! \IQLRSS\Driver::get_ss_opt( 'logging_enabled', 0, true ) ) { … … 37 37 if( class_exists( '\WC_Logger' ) ) { 38 38 39 /** 40 * The WC_Logger does not handle double quotes well. 41 * This will convert double quotes to faux: " -> '' 42 */ 43 array_walk_recursive( $context, function( &$val ) { 44 $val = ( is_string( $val ) ) ? str_replace( '"', "''", $val ) : $val; 45 } ); 46 39 47 $logger = \wc_get_logger(); 40 48 $logger->log( $level, $error_msg, array_merge( $context, array( 'source' => 'live-rates-for-shipstation' ) ) ); -
live-rates-for-shipstation/trunk/live-rates-for-shipstation.php
r3442676 r3452263 4 4 * Plugin URI: https://iqcomputing.com/contact/ 5 5 * Description: ShipStation shipping method with live rates. 6 * Version: 1. 1.27 * Requ ries at least: 6.26 * Version: 1.2.0 7 * Requires at least: 6.2 8 8 * Author: IQComputing 9 9 * Author URI: https://iqcomputing.com/ … … 26 26 * @var String 27 27 */ 28 protected static $version = '1. 1.2';28 protected static $version = '1.2.0'; 29 29 30 30 … … 229 229 Core\Rest_Router::initialize(); 230 230 Core\Settings_Shipstation::initialize(); 231 Core\Admin_Edit_Order::initialize(); 232 231 233 } 232 234 … … 235 237 236 238 /** 237 * Class Autoloader 238 * 239 * @param String $class 239 * Autoload and Drive! 240 240 */ 241 spl_autoload_register( function( $class ) {242 243 if( false === strpos( $class, __NAMESPACE__ . '\\' ) ) {244 return $class;245 }246 247 $class_path = str_replace( __NAMESPACE__ . '\\', '', $class );248 $class_path = str_replace( '_', '-', strtolower( $class_path ) );249 $class_path = str_replace( '\\', '/', $class_path );250 $file_path = wp_normalize_path( sprintf( '%s/%s',251 rtrim( plugin_dir_path( __FILE__ ), '\\/' ),252 $class_path . '.php'253 ) );254 255 if( file_exists( $file_path ) ) {256 require_once $file_path;257 }258 259 } );260 241 require_once rtrim( __DIR__, '\\/' ) . '/_autoload.php'; 261 242 add_action( 'plugins_loaded', array( '\IQLRSS\Driver', 'drive' ), 8 ); -
live-rates-for-shipstation/trunk/readme.txt
r3442676 r3452263 4 4 Requires at least: 5.9 5 5 Tested up to: 6.8 6 Stable tag: 1. 1.26 Stable tag: 1.2.0 7 7 License: GPLv3 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 51 51 == Changelog == 52 52 53 = 1.2.0 (2026-02-02) = 54 * Adds Warehouse Support (Global and Zone based). 55 * Adds ShipStation Packages into Custom Packages on a Shipping Zone. 56 * Moves shipping calculations into a dedicated class with filer hook to override. 57 * New `iqlrss/cache/shipstation` filter hook. 58 * New `iqlrss/cache/shipstation_expires` filter hook. 59 * New `iqlrss/cache/cart_rates` filter hook. 60 * New `iqlrss/shipping/calculator_object` filter hook. 61 53 62 = 1.1.2 (2026-01-19) = 54 63 * Patched an issue where rate caching would not account for a destination change. … … 60 69 = 1.1.1 (2025-12-04) = 61 70 * Fixed JS conflict with WordPress 6.9 (nice!) 62 63 = 1.1.0 (2025-12-01) =64 * Redux the Custom Packaging screen and options.65 * Packing option for Weight Only.66 * Packing option for Stacked Vertically.67 * Packing option for default product weight.68 * Custom Package Presets from UPS, FedEx, and USPS.69 * New filter hook for Shipping Zone Settings `iqlrss/zone/settings`. Useful for managing Product Packing options.70 * New filter hook for Shipping Zone Settings `iqlrss/zone/package_presets`. Useful for managing Custom Package presets.71 * New filter hook for Shipping Estimates `iqlrss/shipping/packages`. Useful for modifying what gets sent to ShipStation API for retrieving shipping estimates.
Note: See TracChangeset
for help on using the changeset viewer.