Changeset 3407166
- Timestamp:
- 12/01/2025 07:11:22 PM (4 months ago)
- Location:
- live-rates-for-shipstation
- Files:
-
- 44 added
- 12 deleted
- 20 edited
- 1 copied
-
tags/1.1.0 (copied) (copied from live-rates-for-shipstation/trunk)
-
tags/1.1.0/README.md (modified) (3 diffs)
-
tags/1.1.0/_stallation.php (modified) (3 diffs)
-
tags/1.1.0/changelog.txt (modified) (1 diff)
-
tags/1.1.0/core/api (added)
-
tags/1.1.0/core/api/shipstation.php (added)
-
tags/1.1.0/core/api/shipstationv1.php (added)
-
tags/1.1.0/core/assets/admin.css (deleted)
-
tags/1.1.0/core/assets/admin.js (deleted)
-
tags/1.1.0/core/assets/css (added)
-
tags/1.1.0/core/assets/css/admin.css (added)
-
tags/1.1.0/core/assets/js (added)
-
tags/1.1.0/core/assets/js/admin.js (added)
-
tags/1.1.0/core/assets/js/integration-settings.js (added)
-
tags/1.1.0/core/assets/js/modules (added)
-
tags/1.1.0/core/assets/js/modules/modal.js (added)
-
tags/1.1.0/core/assets/js/modules/utility.js (added)
-
tags/1.1.0/core/assets/js/shipping-zones (added)
-
tags/1.1.0/core/assets/js/shipping-zones/_main.js (added)
-
tags/1.1.0/core/assets/js/shipping-zones/custom-boxes.js (added)
-
tags/1.1.0/core/assets/json (added)
-
tags/1.1.0/core/assets/json/fedex-packages.json (added)
-
tags/1.1.0/core/assets/json/ups-packages.json (added)
-
tags/1.1.0/core/assets/json/usps-packages.json (added)
-
tags/1.1.0/core/assets/modules (deleted)
-
tags/1.1.0/core/assets/views (added)
-
tags/1.1.0/core/assets/views/customboxes-table.php (added)
-
tags/1.1.0/core/assets/views/services-table.php (added)
-
tags/1.1.0/core/rest-router.php (added)
-
tags/1.1.0/core/settings-shipstation.php (modified) (14 diffs)
-
tags/1.1.0/core/shipping-method-shipstation.php (modified) (40 diffs)
-
tags/1.1.0/core/shipstation-api.php (deleted)
-
tags/1.1.0/core/shipstation-apiv1.php (deleted)
-
tags/1.1.0/core/views (deleted)
-
tags/1.1.0/core/wc-box-packer/class-wc-boxpack-box.php (modified) (6 diffs)
-
tags/1.1.0/core/wc-box-packer/class-wc-boxpack-item.php (modified) (1 diff)
-
tags/1.1.0/core/wc-box-packer/class-wc-boxpack.php (modified) (3 diffs)
-
tags/1.1.0/live-rates-for-shipstation.php (modified) (8 diffs)
-
tags/1.1.0/readme.txt (modified) (3 diffs)
-
trunk/README.md (modified) (3 diffs)
-
trunk/_stallation.php (modified) (3 diffs)
-
trunk/changelog.txt (modified) (1 diff)
-
trunk/core/api (added)
-
trunk/core/api/shipstation.php (added)
-
trunk/core/api/shipstationv1.php (added)
-
trunk/core/assets/admin.css (deleted)
-
trunk/core/assets/admin.js (deleted)
-
trunk/core/assets/css (added)
-
trunk/core/assets/css/admin.css (added)
-
trunk/core/assets/js (added)
-
trunk/core/assets/js/admin.js (added)
-
trunk/core/assets/js/integration-settings.js (added)
-
trunk/core/assets/js/modules (added)
-
trunk/core/assets/js/modules/modal.js (added)
-
trunk/core/assets/js/modules/utility.js (added)
-
trunk/core/assets/js/shipping-zones (added)
-
trunk/core/assets/js/shipping-zones/_main.js (added)
-
trunk/core/assets/js/shipping-zones/custom-boxes.js (added)
-
trunk/core/assets/json (added)
-
trunk/core/assets/json/fedex-packages.json (added)
-
trunk/core/assets/json/ups-packages.json (added)
-
trunk/core/assets/json/usps-packages.json (added)
-
trunk/core/assets/modules (deleted)
-
trunk/core/assets/views (added)
-
trunk/core/assets/views/customboxes-table.php (added)
-
trunk/core/assets/views/services-table.php (added)
-
trunk/core/rest-router.php (added)
-
trunk/core/settings-shipstation.php (modified) (14 diffs)
-
trunk/core/shipping-method-shipstation.php (modified) (40 diffs)
-
trunk/core/shipstation-api.php (deleted)
-
trunk/core/shipstation-apiv1.php (deleted)
-
trunk/core/views (deleted)
-
trunk/core/wc-box-packer/class-wc-boxpack-box.php (modified) (6 diffs)
-
trunk/core/wc-box-packer/class-wc-boxpack-item.php (modified) (1 diff)
-
trunk/core/wc-box-packer/class-wc-boxpack.php (modified) (3 diffs)
-
trunk/live-rates-for-shipstation.php (modified) (8 diffs)
-
trunk/readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
live-rates-for-shipstation/tags/1.1.0/README.md
r3366009 r3407166 6 6 [](https://wordpress.org/plugins/live-rates-for-shipstation/) 7 7 8 Live Rates for ShipStation is a free Open Source plugin that works with [ShipStation](https://www. dpbolvw.net/click-101532691-11646582) and [WooCommerce](https://woocommerce.com/) to pull in shipping estimates from the most common shipping providers.8 Live Rates for ShipStation is a free Open Source plugin that works with [ShipStation](https://www.kqzyfj.com/click-101532691-15733876) and [WooCommerce](https://woocommerce.com/) to pull in shipping estimates from the most common shipping providers. 9 9 10 10 **ShipStation** is a 3rd party provider helping WooCommerce store owners compare shipping carrier rates, automate shipping processes, print labels, sync order data, and group tracking information, among other features. … … 12 12 This plugin connects to the ShipStation API using an authentication key to display shipping rates from various common carriers supported by ShipStation. This allows store owners to group all their shipping carriers under one umbrella which makes management easier and allows customers to choose the best shipping method for them which leads to happier customers. 13 13 14 In order to use the Live Rates for ShipStation plugin, you must have a [premium ShipStation account](https://www. dpbolvw.net/click-101532691-11646582), and purchased the [ShipStation for WooCommerce](https://woocommerce.com/products/shipstation-integration/) plugin. This plugin **will not work** without access to the ShipStation API which is tied to your premium ShipStation account.14 In order to use the Live Rates for ShipStation plugin, you must have a [premium ShipStation account](https://www.kqzyfj.com/click-101532691-15733876), and purchased the [ShipStation for WooCommerce](https://woocommerce.com/products/shipstation-integration/) plugin. This plugin **will not work** without access to the ShipStation API which is tied to your premium ShipStation account. 15 15 16 16 Please review [ShipStations Terms of Service](https://www.shipstation.com/terms-of-service/) and [ShipStations Privacy Policy](https://auctane.com/legal/privacy-policy/) for more information about how your data is managed. 17 17 18 Don't have a ShipStation account? [Open a ShipStation account today!](https://www. dpbolvw.net/click-101532691-11646582)18 Don't have a ShipStation account? [Open a ShipStation account today!](https://www.kqzyfj.com/click-101532691-15733876) 19 19 20 20 ## Requirements … … 22 22 Live Rates for ShipStation is free to use, but it does require a premium ShipStation account to access their REST API. In addition, there are plugin requirements as well. Here's a list of requirements in order to use this plugin properly: 23 23 24 1. [A Premium ShipStation Account](https://www. dpbolvw.net/click-101532691-11646582) (Gold+)24 1. [A Premium ShipStation Account](https://www.kqzyfj.com/click-101532691-15733876) (Gold+) 25 25 1. [WooCommerce Plugin](https://wordpress.org/plugins/woocommerce/) 26 26 1. [ShipStation for WooCommerce Plugin](https://woocommerce.com/products/shipstation-integration/) -
live-rates-for-shipstation/tags/1.1.0/_stallation.php
r3375346 r3407166 15 15 */ 16 16 public static function deactivate() { 17 18 $settings = new Core\Settings_Shipstation(); 19 $settings->clear_cache(); 20 17 \IQLRSS\Driver::clear_cache(); 21 18 } 22 19 … … 27 24 public static function uninstall() { 28 25 29 $settings = new Core\Settings_Shipstation(); 30 $settings->clear_cache(); 31 26 // Normalize ShipStation Settings by removing our keys. 32 27 $settings = get_option( 'woocommerce_shipstation_settings' ); 33 28 foreach( $settings as $key => $val ) { … … 39 34 update_option( 'woocommerce_shipstation_settings', $settings ); 40 35 36 // Clear Cache 37 \IQLRSS\Driver::clear_cache(); 38 39 } 40 41 42 /** 43 * Transition the old plugin version to the current plugin verison. 44 * This may trigger additional actions. 45 * 46 * @param String $version 47 * 48 * @return void 49 */ 50 public static function transversion( $version ) { 51 52 $found_version = \IQLRSS\Driver::get_opt( 'version', '1.0.0' ); 53 if( 0 == version_compare( $version, $found_version ) ) { 54 return; 55 } 56 57 \IQLRSS\Driver::set_opt( 'version', $version ); 58 flush_rewrite_rules(); 59 41 60 } 42 61 -
live-rates-for-shipstation/tags/1.1.0/changelog.txt
r3376459 r3407166 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.1.0 = 6 7 Relase Date: December 01, 2025 8 9 * Overview 10 * Custom Packages really needed to be redone to better support label creation. 11 * Having modal support will make creating custom options / screens easier in future updates. 12 * Having named custom boxes and a modal of options will allow users to better manage product and boxes when requesting a shipping label in a future update. 13 * For example, if products need to be repackaged into different boxes before label creation. 14 * Redo of the Custom Packages options. 15 * New options for Weight Only 16 * New options for Stacked Vertically 17 * New Box Price field. 18 * New Package Presets. 19 * These are pulled from static JSON files + known values. 20 * Support: UPS, FedEx, USPS. 21 * New default product weight field. 22 23 * Code Updates 24 * Filter hook `iqlrss/zone/settings` 25 * Expects array of setting fields. 26 * This hook is useful to manage custom Product Packing options. 27 * core\shipping-method-shipstation.php LN 543 28 * Filter hook `iqlrss/zone/package_presets` 29 * Expects an array of specific key value pairs. 30 * This hook is useful to manage the Custom Package Options when a zone uses this setting. 31 * The carrier_code is important to correctly get One rates from supported carriers. 32 * core\shipping-method-shipstation.php LN 1569 33 * Filter hook `iqlrss/shipping/packages` 34 * Expects an array of ShipStation API v2 /rates/estimate API args. 35 * https://docs.shipstation.com/openapi/rates/estimate_rates 36 * Useful for custom shipping / package rules. This gives developers the cart items to repackage and retrieve rates from. 37 * core\shipping-method-shipstation.php LN 742 38 * Lots of code rearranging, better comments, and better methods to prepare for future updates, features, and functionalty. 4 39 5 40 = 1.0.8 = -
live-rates-for-shipstation/tags/1.1.0/core/settings-shipstation.php
r3375346 r3407166 41 41 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); 42 42 add_action( 'woocommerce_cart_totals_after_order_total',array( $this, 'display_cart_weight' ) ) ; 43 add_action( 'rest_api_init', array( $this, 'api_actions_endpoint' ) );44 43 add_action( 'woocommerce_update_option', array( $this, 'clear_cache_on_update' ) ); 45 46 // Track and Update exported ShipStation Orders47 add_action( 'added_order_meta', array( $this, 'denote_shipstation_export' ), 15, 4 );48 add_action( 'init', array( $this, 'update_exported_orders' ), 15, 4 );49 44 50 45 } … … 61 56 wp_register_style( 62 57 \IQLRSS\Driver::plugin_prefix( 'admin', '-' ), 63 \IQLRSS\Driver::get_asset_url( ' admin.css' ),58 \IQLRSS\Driver::get_asset_url( 'css/admin.css' ), 64 59 array(), 65 60 \IQLRSS\Driver::get( 'version', '1.0.0' ) … … 69 64 wp_register_script_module( 70 65 \IQLRSS\Driver::plugin_prefix( 'admin', '-' ), 71 \IQLRSS\Driver::get_asset_url( ' admin.js' ),66 \IQLRSS\Driver::get_asset_url( 'js/admin.js' ), 72 67 array( 'jquery' ), 73 68 \IQLRSS\Driver::get( 'version', '1.0.0' ) … … 93 88 $data = array( 94 89 'api_verified' => \IQLRSS\Driver::get_ss_opt( 'api_key_valid', false ), 95 'apiv1_verified'=> \IQLRSS\Driver::get_ss_opt( 'apiv1_key_valid', false ),96 90 'global_adjustment_type' => \IQLRSS\Driver::get_ss_opt( 'global_adjustment_type', '' ), 91 'store' => array( 92 'currency_symbol' => get_woocommerce_currency_symbol( get_woocommerce_currency() ), 93 ), 97 94 'rest' => array( 98 95 'nonce' => wp_create_nonce( 'wp_rest' ), 99 ' apiactions'=> get_rest_url( null, sprintf( '/%s/v1/apiactions',96 'settings'=> get_rest_url( null, sprintf( '/%s/v1/settings', 100 97 \IQLRSS\Driver::get( 'slug' ) 101 98 ) ), … … 105 102 'button_api_clearcache' => esc_html__( 'Clear API Cache', 'live-rates-for-shipstation' ), 106 103 'confirm_box_removal' => esc_html__( 'Please confirm you would like to completely remove (x) custom boxes.', 'live-rates-for-shipstation' ), 104 'confirm_modal_closure' => esc_html__( 'Changes you made may not be saved. Close modal window?', 'live-rates-for-shipstation' ), 105 'error_field_required' => esc_html__( 'This field is required, please enter a value.', 'live-rates-for-shipstation' ), 106 'error_custombox_json' => esc_html__( 'Something went wrong while saving your data. Please try again.', 'live-rates-for-shipstation' ), 107 107 'error_rest_generic' => esc_html__( 'Something went wrong with the REST Request. Please resave permalinks and try again.', 'live-rates-for-shipstation' ), 108 108 'error_verification_required' => esc_html__( 'Please click the Verify API button to ensure a connection exists.', 'live-rates-for-shipstation' ), 109 'success_custombox_added' => esc_html__( 'The Custom Box has been added to the list successfully!', 'live-rates-for-shipstation' ), 109 110 'desc_global_adjustment_percentage' => esc_html__( 'Example: IF UPS Ground is $7.25 and you input 15% ($1.08), the final shipping rate the customer sees is: $8.33', 'live-rates-for-shipstation' ), 110 111 'desc_global_adjustment_flatrate' => esc_html__( 'Example: IF UPS Ground is $5.50 and you input $2.37, the final shipping rate the customer sees is: $7.87', 'live-rates-for-shipstation' ), … … 196 197 197 198 /** 198 * REST Endpoint to validate the users API Key and clear API caches.199 *200 * @return void201 */202 public function api_actions_endpoint() {203 204 $prefix = \IQLRSS\Driver::get( 'slug' );205 206 // Handle ajax requests207 register_rest_route( "{$prefix}/v1", 'apiactions', array(208 'methods' => array( 'POST' ),209 'permission_callback' => fn() => is_user_logged_in(),210 'callback' => function( $request ) {211 212 $params = $request->get_params();213 if( ! isset( $params['action'] ) || empty( $params['action'] ) ) {214 wp_send_json_error();215 }216 217 switch( $params['action'] ) {218 219 // Clear the API Caches220 case 'clearcache':221 222 // Success!223 $this->clear_cache();224 wp_send_json_success();225 226 break;227 228 229 // Verify API Key230 case 'verify':231 232 // Error - Unknown Type233 if( empty( $params['type'] ) || ! in_array( $params['type'], array( 'v1', 'v2' ) ) ) {234 wp_send_json_error( esc_html__( 'System could not discern API type.', 'live-rates-for-shipstation' ), 401 );235 236 // Error - v1 API missing key or secret.237 } else if( 'v1' == $params['type'] && ( empty( $params['key'] ) || empty( $params['secret'] ) ) ) {238 wp_send_json_error( esc_html__( 'The ShipStation [v1] API required both a valid [v1] key and [v1] secret.', 'live-rates-for-shipstation' ), 401 );239 240 // Error v2 API missing api key.241 } else if( empty( $params['key'] ) ) {242 wp_send_json_error( esc_html__( 'The ShipStation v2 API requires an API key.', 'live-rates-for-shipstation' ), 401 );243 }244 245 $type = sanitize_title( $params['type'] );246 $settings = array(247 'v2' => \IQLRSS\Driver::get_ss_opt( 'api_key' ),248 'v2valid' => \IQLRSS\Driver::get_ss_opt( 'api_key_valid' ),249 'v2valid_time' => \IQLRSS\Driver::get_ss_opt( 'api_key_vt' ),250 'v1' => \IQLRSS\Driver::get_ss_opt( 'apiv1_key' ),251 'v1secret' => \IQLRSS\Driver::get_ss_opt( 'apiv1_secret' ),252 'v1valid' => \IQLRSS\Driver::get_ss_opt( 'apiv1_key_valid' ),253 'v1valid_time' => \IQLRSS\Driver::get_ss_opt( 'apiv1_key_vt' ),254 );255 $keydata = array(256 'old' => array(257 'key' => $settings[ $type ],258 'secret' => $settings['v1secret'],259 ),260 'new' => array(261 'key' => sanitize_text_field( $params['key'] ),262 'secret' => ( ! empty( $params['secret'] ) ) ? sanitize_text_field( $params['secret'] ) : '',263 )264 );265 266 // Only allow verification once a day if the data is the same.267 if( $keydata['old']['key'] == $keydata['new']['key'] ) {268 269 $valid_time = $settings["{$type}valid_time"];270 if( 'v1' == $type ) {271 $valid_time = ( $keydata['old']['secret'] != $keydata['new']['secret'] ) ? 0 : $valid_time;272 }273 274 // Return Early - We don't need to make a call, it is still valid.275 if( ! empty( $valid_time ) && $valid_time >= gmdate( 'Ymd', strtotime( 'today' ) ) ) {276 wp_send_json_success();277 }278 279 }280 281 // Verify the v1 API282 if( 'v1' == $type ) {283 284 // The API requires the keys to exist before being pinged.285 \IQLRSS\Driver::set_ss_opt( 'apiv1_key', $keydata['new']['key'] );286 \IQLRSS\Driver::set_ss_opt( 'apiv1_secret', $keydata['new']['secret'] );287 288 // Ping the stores so that it sets the currently connected store ID.289 $shipStationAPI = new Shipstation_Apiv1();290 $request = $shipStationAPI->get_stores();291 292 // Error - Something went wrong, the API should let us know.293 if( is_wp_error( $request ) || empty( $request ) ) {294 295 // Revert to old key and secret.296 \IQLRSS\Driver::set_ss_opt( 'apiv1_key', $keydata['old']['key'] );297 \IQLRSS\Driver::set_ss_opt( 'apiv1_secret', $keydata['old']['secret'] );298 299 $message = ( is_wp_error( $request ) ) ? $request->get_error_message() : '';300 $code = ( is_wp_error( $request ) ) ? $request->get_error_code() : 400;301 wp_send_json_error( $message, $code );302 303 }304 305 // Success! - Denote v2 validity and valid time.306 \IQLRSS\Driver::set_ss_opt( 'apiv1_key_valid', true );307 \IQLRSS\Driver::set_ss_opt( 'apiv1_key_vt', gmdate( 'Ymd', strtotime( 'today' ) ) );308 wp_send_json_success();309 310 // Verify the v2 API311 } else {312 313 // The API requires the keys to exist before being pinged.314 \IQLRSS\Driver::set_ss_opt( 'api_key', $keydata['new']['key'] );315 316 // Ping the carriers so that they are cached.317 $shipStationAPI = new Shipstation_Api();318 $request = $shipStationAPI->get_carriers();319 320 // Error - Something went wrong, the API should let us know.321 if( is_wp_error( $request ) || empty( $request ) ) {322 323 // Revert to old key.324 \IQLRSS\Driver::get_ss_opt( 'api_key', $keydata['old']['key'] );325 326 $message = ( is_wp_error( $request ) ) ? $request->get_error_message() : '';327 $code = ( is_wp_error( $request ) ) ? $request->get_error_code() : 400;328 wp_send_json_error( $message, $code );329 330 }331 332 // Success! - Denote v2 validity and valid time.333 \IQLRSS\Driver::set_ss_opt( 'api_key_valid', true );334 \IQLRSS\Driver::set_ss_opt( 'api_key_vt', gmdate( 'Ymd', strtotime( 'today' ) ) );335 wp_send_json_success();336 337 }338 339 break;340 }341 342 // Cases should return their own error/success.343 wp_send_json_error();344 }345 ) );346 347 }348 349 350 /**351 199 * Clear the API cache. 352 200 * … … 363 211 * The first WHERE ensures only `_transient_` and the 2nd ensures only our plugins transients. 364 212 */ 365 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s AND option_name LIKE %s", 213 $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE option_name LIKE %s AND option_name LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder 214 $wpdb->options, 366 215 $wpdb->esc_like( '_transient_' ) . '%', 367 216 '%' . $wpdb->esc_like( '_' . \IQLRSS\Driver::get( 'slug' ) . '_' ) . '%' … … 369 218 370 219 // Set transient to clear any WC_Session caches if they are found. 371 $expires = absint( apply_filters( 'wc_session_expiration', DAY_IN_SECONDS * 2 ) ); 220 $expires = absint( apply_filters( 'wc_session_expiration', DAY_IN_SECONDS * 2 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 372 221 set_transient( \IQLRSS\Driver::plugin_prefix( 'wcs_timeout' ), time(), $expires ); 373 222 … … 388 237 } 389 238 390 $this->clear_cache(); 391 392 } 393 394 395 /** 396 * Denote the exported order as a transient. 397 * Use the transient later to update the order via the v1 API. 398 * 399 * @param Integer $meta_id 400 * @param Integer $order_id 401 * @param String $meta_key 402 * @param String $meta_value 403 * 404 * @return void 405 */ 406 public function denote_shipstation_export( $meta_id, $order_id, $meta_key, $meta_value ) { 407 408 if( '_shipstation_exported' != $meta_key || 'yes' != $meta_value ) { 409 return; 410 } 411 412 $trans_key = \IQLRSS\Driver::plugin_prefix( 'exported_orders' ); 413 $order_ids = get_transient( $trans_key ); 414 $order_ids = ( ! empty( $order_ids ) ) ? $order_ids : array(); 415 416 // Return Early - Order ID already exists. 417 if( in_array( $order_id, $order_ids ) ) { 418 return; 419 } 420 421 $order_ids[] = $order_id; 422 set_transient( $trans_key, $order_ids, HOUR_IN_SECONDS ); 423 424 } 425 426 427 /** 428 * If an `_exported_orders` transient exists 429 * Update the order with some better info. 430 * 431 * @return void 432 */ 433 public function update_exported_orders() { 434 435 $trans_key = \IQLRSS\Driver::plugin_prefix( 'exported_orders' ); 436 $order_ids = get_transient( $trans_key ); 437 438 // Return Early - Delete transient, it's empty. 439 if( empty( $order_ids ) || ! is_array( $order_ids ) ) { 440 return delete_transient( $trans_key ); 441 } 442 443 // Grab the oldest order while also priming the WC_Order cache. 444 $wc_orders = wc_get_orders( array( 445 'include' => array_map( 'absint', $order_ids ), 446 'orderby' => 'date', 447 'order' => 'ASC', 448 'limit' => count( $order_ids ), 449 ) ); 450 451 // Return Early - Could't associate WC_Orders with transient order ids. 452 if( empty( $wc_orders ) ) { 453 return delete_transient( $trans_key ); 454 } 455 456 // Prime the cache 457 // API v1 will always cache it's ShipStation data in the WC_Order as metadata. 458 $apiv1 = new Shipstation_Apiv1( true ); 459 $apiv1->get_orders( array( 460 'createDateEnd' => gmdate( 'c', time() ), 461 ) ); 462 463 $api = new Shipstation_Api( true ); 464 $api->create_shipments_from_wc_orders( $wc_orders ); 465 466 return delete_transient( $trans_key ); 239 \IQLRSS\Driver::clear_cache(); 467 240 468 241 } … … 514 287 public function append_shipstation_integration_settings( $fields ) { 515 288 516 $carriers = array(); 289 $carriers = array( 290 '' => esc_html__( 'ShipStation carriers may still be loading...', 'live-rates-for-shipstation' ), 291 ); 517 292 $appended_fields = array(); 518 293 … … 520 295 521 296 $carrier_desc = esc_html__( 'Select which ShipStation carriers you would like to see live shipping rates from.', 'live-rates-for-shipstation' ); 522 $ shipStationAPI = new Shipstation_Api();523 $response = $shipStationAPI->get_carriers(); 524 297 $response = ( new Api\Shipstation() )->get_carriers(); 298 299 $carriers = array(); 525 300 if( is_a( $response, 'WP_Error' ) ) { 526 301 $carriers[''] = $response->get_error_message(); … … 552 327 'default' => '', 553 328 ); 554 555 // $appended_fields[ \IQLRSS\Driver::plugin_prefix( 'apiv1_key' ) ] = array(556 // 'title' => esc_html__( 'ShipStation [v1] API Key', 'live-rates-for-shipstation' ),557 // 'type' => 'password',558 // 'description' => esc_html__( 'See "ShipStation REST API Key" description, but instead of selecting [v2], select [v1].', 'live-rates-for-shipstation' ),559 // 'default' => '',560 // );561 562 // $appended_fields[ \IQLRSS\Driver::plugin_prefix( 'apiv1_secret' ) ] = array(563 // 'title' => esc_html__( 'ShipStation [v1] API Secret', 'live-rates-for-shipstation' ),564 // 'type' => 'password',565 // 'description' => esc_html__( 'The v1 API is _required_ to manage orders. The v2 API handles Live Rates.', 'live-rates-for-shipstation' ),566 // 'default' => '',567 // );568 329 569 330 $appended_fields[ \IQLRSS\Driver::plugin_prefix( 'carriers' ) ] = array( … … 643 404 } 644 405 645 $this->clear_cache(); 646 } 647 648 // No [v1] API Key? Invalid! 649 $apiv1_key_key = \IQLRSS\Driver::plugin_prefix( 'apiv1_key' ); 650 if( ! isset( $settings[ $apiv1_key_key ] ) || empty( $settings[ $apiv1_key_key ] ) ) { 651 652 $settings[ \IQLRSS\Driver::plugin_prefix( 'apiv1_key_valid' ) ] = false; 653 if( isset( $settings[ \IQLRSS\Driver::plugin_prefix( 'apiv1_key_vt' ) ] ) ) { 654 unset( $settings[ \IQLRSS\Driver::plugin_prefix( 'apiv1_key_vt' ) ] ); 655 } 656 657 $this->clear_cache(); 406 \IQLRSS\Driver::clear_cache(); 658 407 } 659 408 … … 745 494 746 495 // Integration > ShipStation settings page 747 $enqueue = ( $enqueue || isset( $_GET, $_GET['section'] ) && 'shipstation' == $_GET['section']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended496 $enqueue = ( $enqueue || ( isset( $_GET, $_GET['section'] ) && 'shipstation' == $_GET['section'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 748 497 749 498 // Overprotective WooCommerce settings page check -
live-rates-for-shipstation/tags/1.1.0/core/shipping-method-shipstation.php
r3376459 r3407166 2 2 /** 3 3 * ShipStation Live Shipping Rates Method 4 * 5 * @todo Consider moving Shipping Calculations into it's own class. 6 * 7 * @link https://www.fedex.com/en-us/shipping/one-rate.html 8 * @link https://www.usps.com/ship/priority-mail.htm#flatrate 9 * @link https://www.ups.com/worldshiphelp/WSA/ENG/AppHelp/mergedProjects/CORE/Codes/Package_Type_Codes.htm 4 10 * 5 11 * :: Action Hooks … … 28 34 29 35 /** 30 * Array of expected dimension keys (width, height, length, weight)31 *32 * @var Array33 */34 protected $dimension_keys = array(35 'width' => 'width',36 'height' => 'height',37 'length' => 'length',38 'weight' => 'weight',39 );40 41 42 /**43 36 * Array of store specific settings. 44 37 * … … 86 79 87 80 $this->plugin_prefix = \IQLRSS\Driver::get( 'slug' ); 88 $this->shipStationApi = new Shipstation_Api();81 $this->shipStationApi = new Api\Shipstation(); 89 82 $this->id = \IQLRSS\Driver::plugin_prefix( 'shipstation' ); 90 83 $this->instance_id = absint( $instance_id ); … … 133 126 */ 134 127 private function action_hooks() { 128 135 129 add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); 130 add_action( 'admin_footer', array( $this, 'hide_zone_setting_fields' ) ); 131 136 132 } 137 133 … … 146 142 ( new \IQLRSS\Core\Settings_Shipstation() )->clear_cache(); 147 143 return parent::process_admin_options(); 144 145 } 146 147 148 /** 149 * Hide Shipping Zone setting fields 150 * 1). Since they're row options we rarely have markup control over. 151 * 2). Since modules load JS a bit later. 152 * 153 * @return void 154 */ 155 public function hide_zone_setting_fields() { 156 157 ?><script type="text/javascript"> 158 159 /* Hide onebox when not set */ 160 if( document.getElementById( 'woocommerce_iqlrss_shipstation_packing' ) ) { ( function() { 161 if( 'onebox' != document.getElementById( 'woocommerce_iqlrss_shipstation_packing' ).value ) { 162 document.getElementById( 'woocommerce_iqlrss_shipstation_packing' ).closest( 'tr' ).nextElementSibling.style.display = 'none'; 163 } 164 } )(); } 165 </script><?php 148 166 149 167 } … … 293 311 if( isset( $rate_arr['other_costs'] ) ) { 294 312 foreach( $rate_arr['other_costs'] as $o_slug => $o_amount ) { 295 $new_display .= sprintf( ' | %s: %s', ucwords( $o_slug), wc_price( $o_amount ) );313 $new_display .= sprintf( ' | %s: %s', ucwords( str_replace( array( '-', '_' ), ' ', $o_slug ) ), wc_price( $o_amount ) ); 296 314 } 297 315 } … … 314 332 $display_arr = array(); 315 333 foreach( $value as $i => $box_arr ) { 334 335 /* translators: %1$d is box/package count (1,2,3). */ 336 $box_name = sprintf( esc_html__( 'Package %1$d', 'live-rates-for-shipstation' ), $i + 1 ); 337 if( ! empty( $box_arr['nickname'] ) ) { 338 $box_name = $box_arr['nickname']; 339 } 316 340 317 341 $names = esc_html__( 'Product', 'live-rates-for-shipstation' ); … … 323 347 }, $box_arr['packed'] ); 324 348 } 325 326 349 $display_arr[] = sprintf( '%s ( %s ) [ %s %s ( %s x %s x %s %s ) ]', 327 328 /* translators: %1$d is box/package count (1,2,3). */ 329 sprintf( esc_html__( 'Package %1$d', 'live-rates-for-shipstation' ), $i + 1 ), 350 $box_name, 330 351 implode( ', ', (array)$names ), 331 352 $box_arr['weight']['value'], … … 388 409 protected function init_instance_form_fields() { 389 410 390 $ this->instance_form_fields = array(411 $settings = array( 391 412 'title' => array( 392 413 'title' => esc_html__( 'Title', 'live-rates-for-shipstation' ), … … 395 416 'default' => esc_html__( 'ShipStation Rates', 'live-rates-for-shipstation' ), 396 417 'desc_tip' => true, 418 ), 419 'minweight' => array( 420 'title' => esc_html__( 'Product Weight Fallback', 'live-rates-for-shipstation' ), 421 'type' => 'text', 422 'description' => esc_html__( 'This value will be used if both weight and dimensions are missing from any given product. ShipStation at minimum needs a product weight to retrieve rates.', 'live-rates-for-shipstation' ), 397 423 ), 398 424 'packing' => array( … … 403 429 'individual' => esc_html__( 'Pack items individually', 'live-rates-for-shipstation' ), 404 430 'wc-box-packer' => esc_html__( 'Pack items using Custom Packing Boxes', 'live-rates-for-shipstation' ), 431 'onebox' => esc_html__( 'Pack items into one package derived from products', 'live-rates-for-shipstation' ), 405 432 ), 406 433 'description' => esc_html__( 'Individually can be more costly. Custom packing boxes will automatically fit as many products in set dimensions lowering shipping costs.', 'live-rates-for-shipstation' ), 434 ), 435 'packing_sub' => array( 436 'title' => esc_html__( 'Package Dimensions', 'live-rates-for-shipstation' ), 437 'type' => 'select', 438 'options' => array( 439 'weightonly' => esc_html__( 'Total weight', 'live-rates-for-shipstation' ), 440 'stacked' => esc_html__( 'Stacked vertically', 'live-rates-for-shipstation' ), 441 ), 442 'description' => esc_html__( 'Stacked vertically - sums product heights and weights, takes largest of other dimensions. Weight only sums product weights and retrieves rates using the total.', 'live-rates-for-shipstation' ), 407 443 ), 408 444 'customboxes' => array( … … 414 450 ); 415 451 452 453 /** 454 * Allow filtering the Shipping Zone settings 455 * 456 * @hook filter 457 * 458 * @param Array $settings 459 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 460 * 461 * @return Array $settings 462 */ 463 $settings = apply_filters( 'iqlrss/zone/settings', $settings, $this ); 464 $this->instance_form_fields = $settings; 465 466 } 467 468 469 /** 470 * Automatic dynamic method inherited from parent. 471 * Generate HTML for custom boxes fields. 472 * 473 * @return String - HTML 474 */ 475 public function generate_customboxes_html() { 476 477 $prefix = $this->plugin_prefix; 478 $show_custom = ( 'wc-box-packer' == $this->get_option( 'packing', 'individual' ) ); 479 $saved_boxes = $this->get_option( 'customboxes', array() ); 480 $packages = $this->get_package_options(); 481 482 ob_start(); 483 include 'assets/views/customboxes-table.php'; 484 return ob_get_clean(); 485 486 } 487 488 489 /** 490 * Validate customboxes. 491 * 492 * @return Array $boxes 493 */ 494 public function validate_customboxes_field() { 495 496 if( ! isset( $_POST['_wpnonce'] ) ) { 497 return; 498 } 499 500 $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ); 501 if( ! wp_verify_nonce( $nonce, 'woocommerce-settings' ) ) { 502 return; 503 } else if( ! isset( $_POST['custombox'] ) || ! is_array( $_POST['custombox'] ) ) { 504 return; 505 } 506 507 // Input sanitized during processing. 508 $posted_boxes = wp_unslash( $_POST['custombox'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 509 510 $boxes = array(); 511 foreach( $posted_boxes as $box_arr ) { 512 513 if( isset( $box_arr['json'] ) ) { 514 515 $json = json_decode( $box_arr['json'], true ); 516 if( empty( $json['outer'] ) ) continue; 517 518 $boxes[] = array( 519 'active' => absint( $json['active'] ), 520 'preset' => ( isset( $json['preset'] ) ) ? sanitize_text_field( $json['preset'] ) : '', 521 'nickname' => sanitize_text_field( $json['nickname'] ), 522 'outer' => array( 523 'length' => floatval( $json['outer']['length'] ), 524 'width' => floatval( $json['outer']['width'] ), 525 'height' => floatval( $json['outer']['height'] ), 526 ), 527 'inner' => array( 528 'length' => floatval( $json['inner']['length'] ), 529 'width' => floatval( $json['inner']['width'] ), 530 'height' => floatval( $json['inner']['height'] ), 531 ), 532 'weight' => floatval( $json['weight'] ), 533 'weight_max'=> floatval( $json['weight_max'] ), 534 'price' => floatval( $json['price'] ), 535 'carrier_code' => ( isset( $json['carrier_code'] ) ) ? sanitize_text_field( $json['carrier_code'] ) : '', 536 ); 537 538 } 539 } 540 541 usort( $boxes, function( $arrA, $arrB ) { 542 return strcasecmp( $arrA['nickname'], $arrB['nickname'] ); 543 } ); 544 545 return $boxes; 546 416 547 } 417 548 … … 459 590 460 591 ob_start(); 461 include 'views/services-table.php'; 462 return ob_get_clean(); 463 464 } 465 466 467 /** 468 * Automatic dynamic method inherited from parent. 469 * Generate HTML for custom boxes fields. 470 * 471 * @return String - HTML 472 */ 473 public function generate_customboxes_html() { 474 475 $prefix = $this->plugin_prefix; 476 $show_custom = ( 'wc-box-packer' == $this->get_option( 'packing', 'individual' ) ); 477 $saved_boxes = $this->get_option( 'customboxes', array() ); 478 479 ob_start(); 480 include 'views/customboxes-table.php'; 592 include 'assets/views/services-table.php'; 481 593 return ob_get_clean(); 482 594 … … 563 675 564 676 565 /**566 * Validate customboxes field.567 *568 * @return Array $boxes569 */570 public function validate_customboxes_field() {571 572 if( ! isset( $_POST['_wpnonce'] ) ) {573 return;574 }575 576 $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) );577 if( ! wp_verify_nonce( $nonce, 'woocommerce-settings' ) ) {578 return;579 } else if( ! isset( $_POST['custombox'] ) || ! is_array( $_POST['custombox'] ) ) {580 return;581 }582 583 // Input sanitized during processing.584 $posted_boxes = wp_unslash( $_POST['custombox'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized585 586 $boxes = array();587 foreach( $posted_boxes as $box_arr ) {588 589 $vals = array_filter( $box_arr, 'is_numeric' );590 if( count( $vals ) < 7 ) continue;591 592 $boxes[] = array(593 'outer' => array(594 'length' => floatval( $box_arr['ol'] ),595 'width' => floatval( $box_arr['ow'] ),596 'height' => floatval( $box_arr['oh'] ),597 ),598 'inner' => array(599 'length' => floatval( $box_arr['il'] ),600 'width' => floatval( $box_arr['iw'] ),601 'height' => floatval( $box_arr['ih'] ),602 ),603 'weight' => floatval( $box_arr['w'] ),604 'weight_max'=> floatval( $box_arr['wm'] ),605 );606 607 }608 609 return $boxes;610 611 }612 613 614 677 615 678 /**------------------------------------------------------------------------------------------------ **/ … … 619 682 * Calculate shipping costs 620 683 * 621 * @param Array $package 684 * @param Array $packages 622 685 * 623 686 * @return void … … 631 694 // Try to pull from cache. This may set $this->rates 632 695 // Return Early - We have cached rates to work with! 633 $ packages_hash = $this->check_packages_rate_cache( $packages );696 $this->check_packages_rate_cache( $packages ); 634 697 if( ! empty( $this->rates ) ) { 635 698 return; … … 642 705 $enabled_services = $this->get_enabled_services(); 643 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' ) ); 644 708 return; 645 709 } … … 669 733 ); 670 734 671 // Individual Packaging 672 if( 'individual' == $packing_type ) { 673 $item_requests = $this->get_individual_requests( $packages['contents'] ); 674 675 // WC Boxed Packaging 676 } else { 677 $item_requests = $this->get_custombox_requests( $packages['contents'] ); 678 } 679 680 // Rates groups shipping estimates by service ID. 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 } 740 741 742 /** 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 * 772 * @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. 776 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 777 * 778 * @return Array $settings 779 */ 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 */ 681 794 $rates = array(); 682 683 /** 684 * This has to be done per package as the other rates endpoint 685 * requires the customers address1 for verification and really 686 * it's not much faster. 687 */ 688 foreach( $item_requests as $item_id => $req ) { 795 foreach( $filtered_requests as $item_id => $req ) { 689 796 690 797 // Create the API request combining the package (weight, dimensions), general request data, and the carrier info. … … 700 807 // Continue - Something went wrong, should be logged on the API side. 701 808 $available_rates = $this->shipStationApi->get_shipping_estimates( $api_request ); 809 702 810 if( is_wp_error( $available_rates ) || empty( $available_rates ) ) { 703 811 continue; … … 711 819 } 712 820 821 $ratehash = md5( sprintf( '%s%s', $shiprate['code'], $shiprate['carrier_id'] ) ); 713 822 $service_arr = $enabled_services[ $shiprate['carrier_id'] ][ $shiprate['code'] ]; 714 $cost = floatval( $shiprate['cost'] ); 715 $ratemeta = array( 716 '_name'=> ( isset( $req['_name'] ) ) ? $req['_name'] : '', // Item product name. 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. 717 828 'rate' => $cost, 718 829 ); … … 767 878 } 768 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 769 886 // Maybe apply per item. 770 887 if( 'individual' == $packing_type ) { … … 774 891 775 892 // Set rate or append the estimated item ship cost. 776 if( ! isset( $rates[ $ shiprate['code']] ) ) {777 778 $rates[ $ shiprate['code']] = array(779 'id' => $ shiprate['code'],893 if( ! isset( $rates[ $ratehash ] ) ) { 894 895 $rates[ $ratehash ] = array( 896 'id' => $ratehash, 780 897 'label' => ( ! empty( $service_arr['nickname'] ) ) ? $service_arr['nickname'] : $shiprate['name'], 781 898 'package' => $packages, … … 795 912 796 913 } else { 797 $rates[ $ shiprate['code']]['cost'][] = $cost;914 $rates[ $ratehash ]['cost'][] = $cost; 798 915 } 799 916 800 917 // Merge item rates 801 $rates[ $ shiprate['code']]['meta_data']['rates'] = array_merge(802 $rates[ $ shiprate['code']]['meta_data']['rates'],918 $rates[ $ratehash ]['meta_data']['rates'] = array_merge( 919 $rates[ $ratehash ]['meta_data']['rates'], 803 920 array( $ratemeta ), 804 921 ); 805 922 806 923 // Merge item boxes 807 $rates[ $ shiprate['code']]['meta_data']['boxes'] = array_merge(808 $rates[ $ shiprate['code']]['meta_data']['boxes'],924 $rates[ $ratehash ]['meta_data']['boxes'] = array_merge( 925 $rates[ $ratehash ]['meta_data']['boxes'], 809 926 array( $req ), 810 927 ); … … 814 931 } 815 932 816 $single_lowest = \IQLRSS\Driver::get_ss_opt( 'return_lowest', 'no' );817 $single_lowest_label = \IQLRSS\Driver::get_ss_opt( 'return_lowest_label', '' );933 $single_lowest = \IQLRSS\Driver::get_ss_opt( 'return_lowest', 'no' ); 934 $single_lowest_label = \IQLRSS\Driver::get_ss_opt( 'return_lowest_label', '' ); 818 935 819 936 // Add all shipping rates, let the user decide. … … 822 939 foreach( $rates as $rate_arr ) { 823 940 824 // Skip incomplete rate requests 825 if( count( $item_requests ) != count( $rate_arr['cost'] ) ) { 826 continue; 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'] ); 827 945 } 828 946 829 947 // WooCommerce skips serialized data when outputting order item meta, this is a workaround. 830 948 // See hooks above for formatting. 831 $rate_arr['meta_data']['rates'] = json_encode( $rate_arr['meta_data']['rates'] );832 $rate_arr['meta_data']['boxes'] = json_encode( $rate_arr['meta_data']['boxes'] );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'] ); 833 951 834 952 $this->add_rate( $rate_arr ); … … 855 973 // WooCommerce skips serialized data when outputting order item meta, this is a workaround. 856 974 // See hooks above for formatting. 857 $rates[ $lowest_service ]['meta_data']['rates'] = json_encode( $rates[ $lowest_service ]['meta_data']['rates'] );858 $rates[ $lowest_service ]['meta_data']['boxes'] = json_encode( $rates[ $lowest_service ]['meta_data']['boxes'] );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'] ); 859 977 860 978 $this->add_rate( $rates[ $lowest_service ] ); … … 862 980 } 863 981 864 // Add a cache key to check against. 865 WC()->session->set( $this->plugin_prefix, array_merge( 866 WC()->session->get( $this->plugin_prefix, array() ), 867 array( 'method_hash' => $packages_hash ), 982 $cachehash = $this->generate_packages_cache_key( $packages ); 983 if( empty( $cachehash ) ) return; 984 985 // Cache packages to prevent multiple requests. 986 WC()->session->set( $this->plugin_prefix . '_packages', array_merge( 987 WC()->session->get( $this->plugin_prefix . '_packages', array() ), 988 array( 'method_hash' => $cachehash ), 868 989 array( 'method_cache_time' => time() ), 869 990 ) ); … … 879 1000 * @return Array $requests 880 1001 */ 881 protected function get_individual_requests( $items ) { 882 883 $item_requests = array(); 1002 public function group_requestsby_individual( $items ) { 1003 1004 $item_requests = array(); 1005 $default_weight = $this->get_option( 'minweight', '' ); 1006 884 1007 foreach( $items as $item_id => $item ) { 885 1008 … … 894 1017 $item['data']->get_name(), 895 1018 ), 1019 'weight' => ( ! empty( $item['data']->get_weight() ) ) ? $item['data']->get_weight() : $default_weight, 896 1020 ); 897 1021 $physicals = array_filter( array( 898 'weight' => $item['data']->get_weight(),899 1022 'length' => $item['data']->get_length(), 900 1023 'width' => $item['data']->get_width(), … … 903 1026 904 1027 // Return Early - Product missing one of the 4 key dimensions. 905 if( count( $physicals ) < 4) {1028 if( count( $physicals ) < 3 || empty( $request['weight'] ) ) { 906 1029 $this->log( sprintf( 907 1030 908 1031 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */ 909 esc_html__( 'Product ID #%1$d missing (%2$s) dimensions. Shipping calculations terminated.', 'live-rates-for-shipstation' ), 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 Box 1075 * Group all the products by weight and get rates by total weight. 1076 * 1077 * @param Array $items 1078 * 1079 * @return Array $requests 1080 */ 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 Verticially 1169 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 // Largest 1178 '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 // Running 1182 '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 $items 1194 * 1195 * @return Array $requests 1196 */ 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' ), 910 1240 $item['product_id'], 911 1241 implode( ', ', array_diff_key( array( … … 913 1243 'height' => 'height', 914 1244 'length' => 'length', 915 'weight' => 'weight',916 1245 ), $physicals ) ) 917 1246 ) ); … … 919 1248 } 920 1249 921 $request['weight'] = array(922 'value' => (float)round( wc_get_weight( $physicals['weight'], $this->store_data['weight_unit'] ), 2 ),923 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),924 );925 926 // Unset weight and sort dimensions927 unset( $physicals['weight'] );928 1250 sort( $physicals ); 929 930 $request['dimensions'] = array(931 'length' => round( wc_get_dimension( $physicals[2], $this->store_data['dim_unit'] ), 2 ),932 'width' => round( wc_get_dimension( $physicals[1], $this->store_data['dim_unit'] ), 2 ),933 'height' => round( wc_get_dimension( $physicals[0], $this->store_data['dim_unit'] ), 2 ),934 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ),935 );936 937 $item_requests[ $item_id ] = $request;938 939 }940 941 return $item_requests;942 943 }944 945 946 /**947 * Return an array of API requests for custom packed boxes.948 * Shoutout to Mike Jolly & Co.949 *950 * @param Array $items951 *952 * @return Array $requests953 */954 protected function get_custombox_requests( $items ) {955 956 if( ! class_exists( '\IQRLSS\WC_Box_Packer\WC_Boxpack' ) ) {957 include_once 'wc-box-packer/class-wc-boxpack.php';958 }959 960 $item_requests = array();961 $wc_boxpack = new WC_Box_Packer\WC_Boxpack();962 $boxes = $this->get_option( 'customboxes', array() );963 964 if( empty( $boxes ) ) {965 $this->log( esc_html__( 'Custom Boxes selected, but no boxes found. Items packed individually', 'live-rates-for-shipstation' ), 'warning' );966 }967 968 // Setup the WC_Boxpack boxes based on user submitted custom boxes.969 foreach( $boxes as $box ) {970 971 $custombox = $wc_boxpack->add_box( $box['outer']['length'], $box['outer']['width'], $box['outer']['height'], $box['weight'] );972 $custombox->set_inner_dimensions( $box['inner']['length'], $box['inner']['width'], $box['inner']['height'] );973 if( $box['weight_max'] ) $custombox->set_max_weight( $box['weight_max'] );974 975 }976 977 // Loop the items, grabs their dimensions, and assocaite them with WC_Boxpack for future packing.978 foreach( $items as $item_id => $item ) {979 980 // Continue - No shipping needed for product.981 if( ! $item['data']->needs_shipping() ) {982 continue;983 }984 985 $data = array();986 $physicals = array_filter( array(987 'weight' => $item['data']->get_weight(),988 'length' => $item['data']->get_length(),989 'width' => $item['data']->get_width(),990 'height' => $item['data']->get_height(),991 ) );992 993 // Return Early - Product missing one of the 4 key dimensions.994 if( count( $physicals ) < 4 ) {995 $this->log( sprintf(996 997 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */998 esc_html__( 'Product ID #%1$d missing (%2$s) dimensions. Shipping calculations terminated.', 'live-rates-for-shipstation' ),999 $item['product_id'],1000 implode( ', ', array_diff_key( array(1001 'width' => 'width',1002 'height' => 'height',1003 'length' => 'length',1004 'weight' => 'weight',1005 ), $physicals ) )1006 ) );1007 return array();1008 }1009 1010 $data['weight'] = (float)round( wc_get_weight( $physicals['weight'], $this->store_data['weight_unit'] ), 2 );1011 1012 // Unset weight to exclude it from sort1013 unset( $physicals['weight'] );1014 sort( $physicals );1015 1016 1251 $data = array( 1017 1252 'length' => round( wc_get_dimension( $physicals[2], $this->store_data['dim_unit'] ), 2 ), 1018 1253 'width' => round( wc_get_dimension( $physicals[1], $this->store_data['dim_unit'] ), 2 ), 1019 1254 'height' => round( wc_get_dimension( $physicals[0], $this->store_data['dim_unit'] ), 2 ), 1020 ) + $data; 1021 1255 'weight' => round( wc_get_weight( $data['weight'], $this->store_data['weight_unit'] ), 2 ), 1256 ); 1257 1258 // Pack Products 1022 1259 for( $i = 0; $i < $item['quantity']; $i++ ) { 1023 1260 $wc_boxpack->add_item( … … 1048 1285 $item_requests[] = array( 1049 1286 'weight' => array( 1050 'value' => $package->weight,1287 'value' => round( $package->weight, 2 ), 1051 1288 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ), 1052 1289 ), 1053 1290 'dimensions' => array( 1054 'length' => $package->length,1055 'width' => $package->width,1056 'height' => $package->height,1291 'length' => round( $package->length, 2 ), 1292 'width' => round( $package->width, 2 ), 1293 'height' => round( $package->height, 2 ), 1057 1294 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ), 1058 1295 ), 1059 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'] : '', 1060 1303 ); 1061 1304 … … 1073 1316 ), 1074 1317 'max_volume' => floatval( $package->width * $package->height * $package->length ), 1318 'data' => ( ! empty( $package->data ) ) ? $package->data : array(), 1075 1319 ); 1076 1320 … … 1087 1331 1088 1332 /** 1089 * Attempt to pull from the WC() Session cache to prevent multiple caclulation 1333 * Set the rates based on cached packages. 1334 * 1335 * Attempt to pull from the WC() Session cache to prevent multiple calculations 1090 1336 * requests, which could unnecessarily ping the API or add duplicate logs. 1091 1337 * This issue is common when dealing with WP Blocks/Gutenberg Editor. … … 1093 1339 * @param Array $packages - Packages in use. 1094 1340 * 1095 * @return String $hash - hash key neded to reset cache.1341 * @return void 1096 1342 */ 1097 1343 protected function check_packages_rate_cache( $packages ) { 1098 1344 1099 $session = WC()->session->get( $this->plugin_prefix , array() );1345 $session = WC()->session->get( $this->plugin_prefix . '_packages', array() ); 1100 1346 $cleartime = get_transient( \IQLRSS\Driver::plugin_prefix( 'wcs_timeout' ) ); 1347 $cachehash = $this->generate_packages_cache_key( $packages ); 1348 1349 // Return Early - Cache cleared or 30 minuites has passed (invalidate cache). 1350 if( isset( $session['method_cache_time'] ) && ( $cleartime > $session['method_cache_time'] || $session['method_cache_time'] < ( time() - ( 30 * 60 ) ) ) ) { 1351 return; 1352 1353 // Return Early- Cart has changed. 1354 } else if( ! isset( $session['method_hash'] ) || empty( $cachehash ) || $session['method_hash'] != $cachehash ) { 1355 return; 1356 } 1357 1358 // Try to populate Rates. 1359 $size = count( $packages ); 1360 for( $i = 0; $i < $size; $i++ ) { 1361 1362 $cache = WC()->session->get( 'shipping_for_package_' . $i, false ); 1363 if( empty( $cache ) || ! is_array( $cache ) ) { 1364 break; 1365 } 1366 $this->rates = array_merge( $cache['rates'], $this->rates ); 1367 1368 } 1369 1370 } 1371 1372 1373 /** 1374 * Generate a hash key based off of the given packages. 1375 * 1376 * @param Array $packages 1377 * 1378 * @return String $hash 1379 */ 1380 protected function generate_packages_cache_key( $packages ) { 1101 1381 1102 1382 $keys = array(); … … 1108 1388 ); 1109 1389 } 1110 $hash = md5( wp_json_encode( $keys ) ) . \WC_Cache_Helper::get_transient_version( 'shipping' ); 1111 1112 // Return Early - Cache cleared or 30 minuites has passed (invalidate cache). 1113 if( isset( $session['method_cache_time'] ) && ( $cleartime > $session['method_cache_time'] || $session['method_cache_time'] < ( time() - ( 30 * 60 ) ) ) ) { 1114 return $hash; 1115 1116 // Return Early- Cart has changed. 1117 } else if( ! isset( $session['method_hash'] ) || $session['method_hash'] != $hash ) { 1118 return $hash; 1119 } 1120 1121 // Try to populate Rates. 1122 $size = count( $packages ); 1123 for( $i = 0; $i < $size; $i++ ) { 1124 1125 $cache = WC()->session->get( 'shipping_for_package_' . $i, false ); 1126 if( empty( $cache ) || ! is_array( $cache ) ) { 1127 break; 1128 } 1129 $this->rates = array_merge( $cache['rates'], $this->rates ); 1130 1131 } 1132 1133 return $hash; 1134 1135 } 1136 1137 1138 /** 1139 * Generate a hash key based off of the given packages. 1140 * 1141 * @param Array $packages 1142 * 1143 * @return String $hash 1144 */ 1145 protected function generate_packages_cache_key( $packages ) { 1146 1147 // Maybe skip if cache was cleared. 1148 $session = WC()->session->get( $this->plugin_prefix, array() ); 1149 $cleartime = get_transient( \IQLRSS\Driver::plugin_prefix( 'wcs_timeout' ) ); 1150 if( isset( $session['method_cache_time'] ) && $cleartime > $session['method_cache_time'] ) { 1151 return ''; 1152 } 1153 1154 $keys = array(); 1155 foreach( $packages['contents'] as $key => $package ) { 1156 $keys[] = array( 1157 $key, 1158 $package['quantity'], 1159 $package['line_total'], 1160 ); 1161 } 1162 1163 $hash = md5( wp_json_encode( $keys ) ) . \WC_Cache_Helper::get_transient_version( 'shipping' ); 1164 return ( ! empty( $keys ) ) ? $hash : ''; 1390 1391 if( empty( $keys ) ) return ''; 1392 return md5( wp_json_encode( $keys ) ) . \WC_Cache_Helper::get_transient_version( 'shipping' ); 1165 1393 1166 1394 } … … 1171 1399 /** :: Helper Methods :: **/ 1172 1400 /**------------------------------------------------------------------------------------------------ **/ 1401 /** 1402 * Map known packages. 1403 * @see assets/json 1404 * 1405 * @param String $key 1406 * 1407 * @return String 1408 */ 1409 public function get_package_label( $key ) { 1410 1411 $labels = array( 1412 // UPS 1413 'flat_rate_envelope' => esc_html__( 'USPS Flat Rate Envelope', 'live-rates-for-shipstation' ), 1414 'flat_rate_legal_envelope' => esc_html__( 'USPS Flat Rate Legal Envelope', 'live-rates-for-shipstation' ), 1415 'flat_rate_padded_envelope' => esc_html__( 'USPS Flat Rate Padded Envelope', 'live-rates-for-shipstation' ), 1416 'large_envelope_or_flat'=> esc_html__( 'USPS Large Envelope or Flat', 'live-rates-for-shipstation' ), 1417 'large_flat_rate_box' => esc_html__( 'USPS Large Flat Rate Box', 'live-rates-for-shipstation' ), 1418 'medium_flat_rate_box' => esc_html__( 'USPS Medium Flat Rate Box', 'live-rates-for-shipstation' ), 1419 'small_flat_rate_box' => esc_html__( 'USPS Small Flat Rate Box', 'live-rates-for-shipstation' ), 1420 'regional_rate_box_a' => esc_html__( 'USPS Regional Rate Box A', 'live-rates-for-shipstation' ), 1421 'regional_rate_box_b' => esc_html__( 'USPS Regional Rate Box B', 'live-rates-for-shipstation' ), 1422 1423 // USPS 1424 'ups_10_kg_box' => esc_html__( 'UPS 10kg (22lbs) Box', 'live-rates-for-shipstation' ), 1425 'ups_25_kg_box' => esc_html__( 'UPS 25kg (55lbs) Box', 'live-rates-for-shipstation' ), 1426 'ups__express_box_large'=> esc_html__( 'UPS Express Box - Large', 'live-rates-for-shipstation' ), // Why does this have an extra underscore? Ask ShipStation. 1427 'ups_express_box_medium'=> esc_html__( 'UPS Express Box - Medium', 'live-rates-for-shipstation' ), 1428 'ups_express_box_small' => esc_html__( 'UPS Express Box - Small', 'live-rates-for-shipstation' ), 1429 'ups_tube' => esc_html__( 'UPS Tube', 'live-rates-for-shipstation' ), 1430 'ups_express_pak' => esc_html__( 'UPS Express Pak', 'live-rates-for-shipstation' ), 1431 'ups_letter' => esc_html__( 'UPS Letter', 'live-rates-for-shipstation' ), 1432 1433 // FedEx 1434 'fedex_10kg_box' => esc_html__( 'FedEx 10kg (22lbs) Box', 'live-rates-for-shipstation' ), 1435 'fedex_25kg_box' => esc_html__( 'FedEx 25kg (55lbs) Box', 'live-rates-for-shipstation' ), 1436 'fedex_extra_large_box' => esc_html__( 'FedEx Extra Large Box', 'live-rates-for-shipstation' ), 1437 'fedex_large_box' => esc_html__( 'FedEx Large Box', 'live-rates-for-shipstation' ), 1438 'fedex_medium_box' => esc_html__( 'FedEx Medium Box', 'live-rates-for-shipstation' ), 1439 'fedex_small_box' => esc_html__( 'FedEx Small Box', 'live-rates-for-shipstation' ), 1440 'fedex_tube' => esc_html__( 'FedEx Tube', 'live-rates-for-shipstation' ), 1441 'fedex_envelope' => esc_html__( 'FedEx Envelope', 'live-rates-for-shipstation' ), 1442 'fedex_pak' => esc_html__( 'FedEx Padded Pak', 'live-rates-for-shipstation' ), 1443 ); 1444 1445 return ( isset( $labels [ $key ] ) ) ? $labels[ $key ] : esc_html__( 'Unknown Package', 'live-rates-for-shipstation' ); 1446 1447 } 1448 1449 1173 1450 /** 1174 1451 * Return an array of Price Adjustment Type options. … … 1214 1491 1215 1492 /** 1493 * Convert a WooCommerce unit to a ShipStation unit. 1494 * 1495 * @param String $unit 1496 * 1497 * @return String $new_unit 1498 */ 1499 public function convert_unit_term( $unit ) { 1500 return $this->shipStationApi->convert_unit_term( $unit ); 1501 } 1502 1503 1504 /** 1505 * Return an array of package options. 1506 * 1507 * @return Array 1508 */ 1509 protected function get_package_options() { 1510 1511 $packages = wp_cache_get( 'packages', $this->plugin_prefix ); 1512 if( ! empty( $packages ) ) { 1513 return $packages; 1514 } 1515 1516 $global_carriers= $this->shipStationApi->get_carriers(); 1517 $carrier_codes = wp_list_pluck( $global_carriers, 'carrier_code' ); 1518 $carrier_codes = array_intersect_key( $carrier_codes, array_flip( $this->carriers ) ); 1519 1520 $data = array( 1521 'usps' => array( 1522 'label' => esc_html__( 'USPS', 'live-rates-for-shipstation' ), 1523 'packages' => json_decode( file_get_contents( \IQLRSS\Driver::get_asset_path( 'json/usps-packages.json' ) ), true ), 1524 ), 1525 'ups' => array( 1526 'label' => esc_html__( 'UPS', 'live-rates-for-shipstation' ), 1527 'packages' => json_decode( file_get_contents( \IQLRSS\Driver::get_asset_path( 'json/ups-packages.json' ) ), true ), 1528 ), 1529 'fedex' => array( 1530 'label' => esc_html__( 'FedEx', 'live-rates-for-shipstation' ), 1531 'packages' => json_decode( file_get_contents( \IQLRSS\Driver::get_asset_path( 'json/fedex-packages.json' ) ), true ), 1532 ), 1533 ); 1534 1535 // Append Translated Labels 1536 $carrier_packages = array(); 1537 foreach( $data as $carrier_code => &$carriers ) { 1538 1539 // Match carrier slug with known carrier code. 1540 $carrier_found = array_filter( $carrier_codes, fn( $c ) => $c === $carrier_code ); 1541 if( empty( $carrier_found ) ) { 1542 $carrier_found = array_filter( $carrier_codes, fn( $c ) => false !== strpos( $c, $carrier_code . '_' ) ); 1543 } 1544 1545 // Skip - Carrier may not be set. 1546 if( empty( $carrier_found ) ) continue; 1547 1548 $codes = wp_list_pluck( $carriers['packages'], 'code' ); 1549 $dupes = array_count_values( $codes ); 1550 1551 foreach( $carriers['packages'] as &$package ) { 1552 1553 $package['carrier_code'] = $carrier_code; 1554 $package['label'] = $this->get_package_label( $package['code'] ); 1555 1556 if( $dupes[ $package['code'] ] > 1 ) { 1557 $package['label'] .= sprintf( ' (%s x %s x %s)', $package['length'], $package['width'], $package['height'] ); 1558 } 1559 } 1560 1561 usort( $carriers['packages'], fn( $pa, $pb ) => strcmp( $pa['label'], $pb['label'] ) ); 1562 $carrier_packages[ $carrier_code ] = $carriers; 1563 1564 } 1565 1566 $data = array( '' => esc_html__( '-- Select Package Preset --', 'live-rates-for-shipstation' ) ) + $carrier_packages; 1567 1568 1569 /** 1570 * Allow hooking into Custom Package presets for 3rd party management. 1571 * 1572 * @hook filter 1573 * 1574 * @param Array $data - Array( Array( 1575 * 'label' => 'Optional Optgroup Name', 1576 * 'packages' => Array( 1577 * 'label' => '', 1578 * 'code' => '', 1579 * 'length' => 0, 1580 * 'width' => 0, 1581 * 'height' => 0, 1582 * 'weight_max' => 0, 1583 * 'carrier_code' => '', 1584 * ) 1585 * ) ) 1586 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 1587 * 1588 * @return Array $data 1589 */ 1590 $packages = apply_filters( 'iqlrss/zone/package_presets', $data, $this ); 1591 1592 // Maybe reset if what we're given is not what we expect. 1593 if( ! is_array( $packages ) ) $packages = $data; 1594 1595 // Cache results to avoid multiple file reads per request. 1596 if( ! empty( $packages ) ) { 1597 wp_cache_add( 'packages', $packages, $this->plugin_prefix ); 1598 1599 // Maybe provide a default options / text when empty. 1600 } else { 1601 $packages = array( '' => esc_html__( 'No package presets.', 'live-rates-for-shipstation' ) ); 1602 } 1603 1604 return $packages; 1605 1606 } 1607 1608 1609 /** 1216 1610 * Format a stringified product name. 1217 1611 * ex. 213|Shirt|optional|meta|data … … 1223 1617 * @return String $name 1224 1618 */ 1225 p ublicfunction format_shipitem_name( $shipitem_name, $link = false, $context = 'edit' ) {1619 protected function format_shipitem_name( $shipitem_name, $link = false, $context = 'edit' ) { 1226 1620 1227 1621 $name = mb_strimwidth( $shipitem_name, 0, 47, '...' ); -
live-rates-for-shipstation/tags/1.1.0/core/wc-box-packer/class-wc-boxpack-box.php
r3376459 r3407166 2 2 /** 3 3 * Box Packing class found in woocommerce-shipping-ups 4 * Updated by IQComputing because many of these methods 5 * have the wrong return documentation. 4 * Updated by IQComputing 6 5 * 7 6 * @version 2.0.1 … … 68 67 69 68 /** 70 * __construct function. 71 * 72 * @access public 69 * Box info - contains the core box properties and 70 * additonal data like nickname and price. 71 * See the Shipping Method Custom Packing Boxes for more info. 72 * 73 * @var Array 74 */ 75 private $data = array(); 76 77 78 /** 79 * Setup box properties. 80 * 81 * @param Array $box - Array( 'outer' => array( 'length', 'width', 'height' ), 'inner' => array( see outer ) ) 82 * 73 83 * @return void 74 84 */ 75 public function __construct( $length, $width, $height, $weight = 0 ) { 76 $dimensions = array( $length, $width, $height ); 77 78 sort( $dimensions ); 79 80 $this->outer_length = $this->length = floatval( $dimensions[2] ); 81 $this->outer_width = $this->width = floatval( $dimensions[1] ); 82 $this->outer_height = $this->height = floatval( $dimensions[0] ); 83 $this->weight = floatval( $weight ); 85 public function __construct( $box ) { 86 87 // Default - All Outer 88 $this->length = floatval( $box['outer']['length'] ); 89 $this->width = floatval( $box['outer']['width'] ); 90 $this->height = floatval( $box['outer']['height'] ); 91 $this->outer_length = floatval( $box['outer']['length'] ); 92 $this->outer_width = floatval( $box['outer']['width'] ); 93 $this->outer_height = floatval( $box['outer']['height'] ); 94 95 // Inner 96 if( ! empty( array_filter( (array)$box['inner'] ) ) ) { 97 $this->length = floatval( $box['inner']['length'] ); 98 $this->width = floatval( $box['inner']['width'] ); 99 $this->height = floatval( $box['inner']['height'] ); 100 } 101 102 // Weight 103 $this->weight = floatval( $box['weight'] ); 104 105 // Everything else 106 $this->data = $box; 107 84 108 } 85 109 … … 211 235 $this->reset_packed_dimensions(); 212 236 213 // @todo Rememer this kind of loop, neat method, love it.214 237 while ( sizeof( $items ) > 0 ) { 215 238 $item = array_shift( $items ); … … 268 291 $package->height = $this->get_outer_height(); 269 292 $package->value = $packed_value; 293 $package->data = $this->data; 270 294 271 295 // Calculate packing success % based on % of weight and volume of all items packed … … 281 305 } 282 306 307 // Fallback to amount packed 283 308 if ( is_null( $packed_weight_ratio ) && is_null( $packed_volume_ratio ) ) { 284 // Fallback to amount packed285 309 $package->percent = ( sizeof( $packed ) / ( sizeof( $unpacked ) + sizeof( $packed ) ) ) * 100; 310 311 // Volume only 286 312 } elseif ( is_null( $packed_weight_ratio ) ) { 287 // Volume only288 313 $package->percent = $packed_volume_ratio * 100; 314 315 // Weight only 289 316 } elseif ( is_null( $packed_volume_ratio ) ) { 290 // Weight only291 317 $package->percent = $packed_weight_ratio * 100; 318 319 // Default? 292 320 } else { 293 321 $package->percent = $packed_weight_ratio * $packed_volume_ratio * 100; … … 388 416 return $this->packed_length; 389 417 } 418 419 /** 420 * Return box data. 421 * 422 * @param String $key 423 * @param Mixed $default 424 * 425 * @return Mixed 426 */ 427 public function get_data( $key, $default = '' ) { 428 return ( isset( $this->data[ $key ] ) ) ? $this->data[ $key ] : $default; 429 } 390 430 } -
live-rates-for-shipstation/tags/1.1.0/core/wc-box-packer/class-wc-boxpack-item.php
r3339099 r3407166 2 2 /** 3 3 * Box Packing class found in woocommerce-shipping-ups 4 * Updated by IQComputing because many of these methods 5 * have the wrong return documentation. 4 * Updated by IQComputing 6 5 * 7 6 * @version 2.0.1 -
live-rates-for-shipstation/tags/1.1.0/core/wc-box-packer/class-wc-boxpack.php
r3376459 r3407166 2 2 /** 3 3 * Box Packing class found in woocommerce-shipping-ups 4 * Updated by IQComputing because many of these methods 5 * have the wrong return documentation. 4 * Updated by IQComputing 6 5 * 7 6 * @version 2.0.1 … … 70 69 * 71 70 * @access public 72 * @param mixed $length 73 * @param mixed $width 74 * @param mixed $height 75 * @param mixed $weight 71 * @param Array $box - Array( 'outer' => array( 'length', 'width', 'height' ), 'inner' => array( see outer ) ) 76 72 * @return object WC_Boxpack_Box 77 73 */ 78 public function add_box( $ length, $width, $height, $weight = 0) {79 $new_box = new WC_Boxpack_Box( $ length, $width, $height, $weight);74 public function add_box( $box ) { 75 $new_box = new WC_Boxpack_Box( $box ); 80 76 $this->boxes[] = $new_box; 81 77 return $new_box; … … 174 170 $package->unpacked = true; 175 171 $package->packed = array( $item ); 172 $package->data = array(); 176 173 $this->packages[] = $package; 177 174 } -
live-rates-for-shipstation/tags/1.1.0/live-rates-for-shipstation.php
r3376459 r3407166 4 4 * Plugin URI: https://iqcomputing.com/contact/ 5 5 * Description: ShipStation shipping method with live rates. 6 * Version: 1. 0.87 * Requries at least: 5.96 * Version: 1.1.0 7 * Requries at least: 6.2 8 8 * Author: IQComputing 9 9 * Author URI: https://iqcomputing.com/ … … 12 12 * Text Domain: live-rates-for-shipstation 13 13 * Requires Plugins: woocommerce, woocommerce-shipstation-integration 14 *15 * @notes ShipStation does not make it easy or obvious how to update / create a Shipment for an Order.16 * The shipment create endpoint keeps coming back successful, but nothing on the ShipStation side17 * appears to change.18 * The v1 API update Order endpoint also doesn't seem to allow Shipment updates, but is required19 * to get the OrderID, required for any kind of create/update endpoints.20 *21 * @todo Add warehosue locations to Shipping Zone packages.22 * @todo Look into updating warehouses through Edit Order > Order Items.23 14 */ 24 15 namespace IQLRSS; … … 35 26 * @var String 36 27 */ 37 protected static $version = '1. 0.8';28 protected static $version = '1.1.0'; 38 29 39 30 … … 86 77 * @param Mixed $value 87 78 * 88 * @return Mixed79 * @return void 89 80 */ 90 81 public static function set_ss_opt( $key, $value ) { … … 105 96 106 97 /** 98 * Return a ShipStation Plugin Option Value 99 * 100 * @param String $key 101 * @param Mixed $default 102 * @param Boolean $skip_prefix - Skip Plugin Prefix and return a core ShipStation setting value. 103 * 104 * @return Mixed 105 */ 106 public static function get_opt( $key, $default = '' ) { 107 $settings = get_option( static::plugin_prefix( 'plugin' ) ); 108 return ( isset( $settings[ $key ] ) && '' !== $settings[ $key ] ) ? maybe_unserialize( $settings[ $key ] ) : $default; 109 } 110 111 112 /** 113 * Set a plugin option. 114 * 115 * @param String $key 116 * @param Mixed $value 117 * 118 * @return void 119 */ 120 public static function set_opt( $key, $value ) { 121 122 $option = static::plugin_prefix( 'plugin' ); 123 $settings = get_option( $option, array() ); 124 125 if( is_bool( $value ) ) { 126 $settings[ $key ] = boolval( $value ); 127 } else if( is_string( $value ) || is_numeric( $value ) ) { 128 $settings[ $key ] = sanitize_text_field( $value ); 129 } 130 131 update_option( $option, $settings ); 132 133 } 134 135 136 /** 137 * Clear the Plugin API cache. 138 * 139 * @return void 140 */ 141 public static function clear_cache() { 142 143 global $wpdb; 144 145 /** 146 * The API Class creates various transients to cache carrier services. 147 * These transients are not tracked but generated based on the responses carrier codes. 148 * All these transients are prefixed with our plugins unique string slug. 149 * The first WHERE ensures only `_transient_` and the 2nd ensures only our plugins transients. 150 */ 151 $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE option_name LIKE %s AND option_name LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder 152 $wpdb->options, 153 $wpdb->esc_like( '_transient_' ) . '%', 154 '%' . $wpdb->esc_like( '_' . static::get( 'slug' ) . '_' ) . '%' 155 ) ); 156 157 // Set transient to clear any WC_Session caches if they are found. 158 $expires = absint( apply_filters( 'wc_session_expiration', DAY_IN_SECONDS * 2 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 159 set_transient( static::plugin_prefix( 'wcs_timeout' ), time(), $expires ); 160 161 } 162 163 164 /** 107 165 * Prefix a string with the plugin slug. 108 166 * … … 124 182 125 183 /** 126 * Return a URL to an asset (JS/CSS )184 * Return a URL to an asset (JS/CSS usually) 127 185 * 128 186 * @param String $asset 129 187 * 130 * @return String $url188 * @return String 131 189 */ 132 190 public static function get_asset_url( $asset ) { … … 141 199 142 200 /** 201 * Return a path to an asset. 202 * 203 * @param String $asset 204 * 205 * @return String 206 */ 207 public static function get_asset_path( $asset ) { 208 209 return sprintf( '%s/core/assets/%s', 210 rtrim( plugin_dir_path( __FILE__ ), '\\/' ), 211 $asset 212 ); 213 214 } 215 216 217 /** 143 218 * Initialize the core controllers 144 219 * Vroom! … … 147 222 */ 148 223 public static function drive() { 224 225 // Run any version transition actions. 226 Stallation::transversion( static::$version ); 227 228 // Load core controllers. 229 Core\Rest_Router::initialize(); 149 230 Core\Settings_Shipstation::initialize(); 150 231 } -
live-rates-for-shipstation/tags/1.1.0/readme.txt
r3376459 r3407166 4 4 Requires at least: 5.9 5 5 Tested up to: 6.8 6 Stable tag: 1. 0.86 Stable tag: 1.1.0 7 7 License: GPLv3 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 16 16 This plugin connects to the ShipStation API using an authentication key to display shipping rates from various common carriers supported by ShipStation. This allows store owners to group all their shipping carriers under one umbrella which makes management easier and allows customers to choose the best shipping method for them which leads to happier customers. 17 17 18 In order to use the Live Rates for ShipStation plugin, you must have a [premium ShipStation account](https://www. dpbolvw.net/click-101532691-11646582), and purchased the [ShipStation for WooCommerce](https://woocommerce.com/products/shipstation-integration/) plugin. This plugin **will not work** without access to the ShipStation API which is tied to your premium ShipStation account.18 In order to use the Live Rates for ShipStation plugin, you must have a [premium ShipStation account](https://www.kqzyfj.com/click-101532691-15733876), and purchased the [ShipStation for WooCommerce](https://woocommerce.com/products/shipstation-integration/) plugin. This plugin **will not work** without access to the ShipStation API which is tied to your premium ShipStation account. 19 19 20 20 Please review [ShipStations Terms of Service](https://www.shipstation.com/terms-of-service/) and [ShipStations Privacy Policy](https://auctane.com/legal/privacy-policy/) for more information about how your data is managed. 21 21 22 Don't have a ShipStation account? [Open a ShipStation account today!](https://www. dpbolvw.net/click-101532691-11646582)22 Don't have a ShipStation account? [Open a ShipStation account today!](https://www.kqzyfj.com/click-101532691-15733876) 23 23 24 24 == Plugin Requirements == 25 25 26 1. [A Premium ShipStation Account](https://www. dpbolvw.net/click-101532691-11646582)26 1. [A Premium ShipStation Account](https://www.kqzyfj.com/click-101532691-15733876) 27 27 1. [The WooCommerce Plugin](https://wordpress.org/plugins/woocommerce/) 28 28 1. [The ShipStation for WooCommerce Plugin](https://woocommerce.com/products/shipstation-integration/) … … 51 51 == Changelog == 52 52 53 = 1.1.0 (2025-12-01) = 54 * Redux the Custom Packaging screen and options. 55 * Packing option for Weight Only. 56 * Packing option for Stacked Vertically. 57 * Packing option for default product weight. 58 * Custom Package Presets from UPS, FedEx, and USPS. 59 * New filter hook for Shipping Zone Settings `iqlrss/zone/settings`. Useful for managing Product Packing options. 60 * New filter hook for Shipping Zone Settings `iqlrss/zone/package_presets`. Useful for managing Custom Package presets. 61 * New filter hook for Shipping Estimates `iqlrss/shipping/packages`. Useful for modifying what gets sent to ShipStation API for retrieving shipping estimates. 62 53 63 = 1.0.8 (2025-10-10) = 54 64 * Patches issue of missing `other_amount` when applying shipping rates (thanks @centuryperf)! -
live-rates-for-shipstation/trunk/README.md
r3366009 r3407166 6 6 [](https://wordpress.org/plugins/live-rates-for-shipstation/) 7 7 8 Live Rates for ShipStation is a free Open Source plugin that works with [ShipStation](https://www. dpbolvw.net/click-101532691-11646582) and [WooCommerce](https://woocommerce.com/) to pull in shipping estimates from the most common shipping providers.8 Live Rates for ShipStation is a free Open Source plugin that works with [ShipStation](https://www.kqzyfj.com/click-101532691-15733876) and [WooCommerce](https://woocommerce.com/) to pull in shipping estimates from the most common shipping providers. 9 9 10 10 **ShipStation** is a 3rd party provider helping WooCommerce store owners compare shipping carrier rates, automate shipping processes, print labels, sync order data, and group tracking information, among other features. … … 12 12 This plugin connects to the ShipStation API using an authentication key to display shipping rates from various common carriers supported by ShipStation. This allows store owners to group all their shipping carriers under one umbrella which makes management easier and allows customers to choose the best shipping method for them which leads to happier customers. 13 13 14 In order to use the Live Rates for ShipStation plugin, you must have a [premium ShipStation account](https://www. dpbolvw.net/click-101532691-11646582), and purchased the [ShipStation for WooCommerce](https://woocommerce.com/products/shipstation-integration/) plugin. This plugin **will not work** without access to the ShipStation API which is tied to your premium ShipStation account.14 In order to use the Live Rates for ShipStation plugin, you must have a [premium ShipStation account](https://www.kqzyfj.com/click-101532691-15733876), and purchased the [ShipStation for WooCommerce](https://woocommerce.com/products/shipstation-integration/) plugin. This plugin **will not work** without access to the ShipStation API which is tied to your premium ShipStation account. 15 15 16 16 Please review [ShipStations Terms of Service](https://www.shipstation.com/terms-of-service/) and [ShipStations Privacy Policy](https://auctane.com/legal/privacy-policy/) for more information about how your data is managed. 17 17 18 Don't have a ShipStation account? [Open a ShipStation account today!](https://www. dpbolvw.net/click-101532691-11646582)18 Don't have a ShipStation account? [Open a ShipStation account today!](https://www.kqzyfj.com/click-101532691-15733876) 19 19 20 20 ## Requirements … … 22 22 Live Rates for ShipStation is free to use, but it does require a premium ShipStation account to access their REST API. In addition, there are plugin requirements as well. Here's a list of requirements in order to use this plugin properly: 23 23 24 1. [A Premium ShipStation Account](https://www. dpbolvw.net/click-101532691-11646582) (Gold+)24 1. [A Premium ShipStation Account](https://www.kqzyfj.com/click-101532691-15733876) (Gold+) 25 25 1. [WooCommerce Plugin](https://wordpress.org/plugins/woocommerce/) 26 26 1. [ShipStation for WooCommerce Plugin](https://woocommerce.com/products/shipstation-integration/) -
live-rates-for-shipstation/trunk/_stallation.php
r3375346 r3407166 15 15 */ 16 16 public static function deactivate() { 17 18 $settings = new Core\Settings_Shipstation(); 19 $settings->clear_cache(); 20 17 \IQLRSS\Driver::clear_cache(); 21 18 } 22 19 … … 27 24 public static function uninstall() { 28 25 29 $settings = new Core\Settings_Shipstation(); 30 $settings->clear_cache(); 31 26 // Normalize ShipStation Settings by removing our keys. 32 27 $settings = get_option( 'woocommerce_shipstation_settings' ); 33 28 foreach( $settings as $key => $val ) { … … 39 34 update_option( 'woocommerce_shipstation_settings', $settings ); 40 35 36 // Clear Cache 37 \IQLRSS\Driver::clear_cache(); 38 39 } 40 41 42 /** 43 * Transition the old plugin version to the current plugin verison. 44 * This may trigger additional actions. 45 * 46 * @param String $version 47 * 48 * @return void 49 */ 50 public static function transversion( $version ) { 51 52 $found_version = \IQLRSS\Driver::get_opt( 'version', '1.0.0' ); 53 if( 0 == version_compare( $version, $found_version ) ) { 54 return; 55 } 56 57 \IQLRSS\Driver::set_opt( 'version', $version ); 58 flush_rewrite_rules(); 59 41 60 } 42 61 -
live-rates-for-shipstation/trunk/changelog.txt
r3376459 r3407166 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.1.0 = 6 7 Relase Date: December 01, 2025 8 9 * Overview 10 * Custom Packages really needed to be redone to better support label creation. 11 * Having modal support will make creating custom options / screens easier in future updates. 12 * Having named custom boxes and a modal of options will allow users to better manage product and boxes when requesting a shipping label in a future update. 13 * For example, if products need to be repackaged into different boxes before label creation. 14 * Redo of the Custom Packages options. 15 * New options for Weight Only 16 * New options for Stacked Vertically 17 * New Box Price field. 18 * New Package Presets. 19 * These are pulled from static JSON files + known values. 20 * Support: UPS, FedEx, USPS. 21 * New default product weight field. 22 23 * Code Updates 24 * Filter hook `iqlrss/zone/settings` 25 * Expects array of setting fields. 26 * This hook is useful to manage custom Product Packing options. 27 * core\shipping-method-shipstation.php LN 543 28 * Filter hook `iqlrss/zone/package_presets` 29 * Expects an array of specific key value pairs. 30 * This hook is useful to manage the Custom Package Options when a zone uses this setting. 31 * The carrier_code is important to correctly get One rates from supported carriers. 32 * core\shipping-method-shipstation.php LN 1569 33 * Filter hook `iqlrss/shipping/packages` 34 * Expects an array of ShipStation API v2 /rates/estimate API args. 35 * https://docs.shipstation.com/openapi/rates/estimate_rates 36 * Useful for custom shipping / package rules. This gives developers the cart items to repackage and retrieve rates from. 37 * core\shipping-method-shipstation.php LN 742 38 * Lots of code rearranging, better comments, and better methods to prepare for future updates, features, and functionalty. 4 39 5 40 = 1.0.8 = -
live-rates-for-shipstation/trunk/core/settings-shipstation.php
r3375346 r3407166 41 41 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); 42 42 add_action( 'woocommerce_cart_totals_after_order_total',array( $this, 'display_cart_weight' ) ) ; 43 add_action( 'rest_api_init', array( $this, 'api_actions_endpoint' ) );44 43 add_action( 'woocommerce_update_option', array( $this, 'clear_cache_on_update' ) ); 45 46 // Track and Update exported ShipStation Orders47 add_action( 'added_order_meta', array( $this, 'denote_shipstation_export' ), 15, 4 );48 add_action( 'init', array( $this, 'update_exported_orders' ), 15, 4 );49 44 50 45 } … … 61 56 wp_register_style( 62 57 \IQLRSS\Driver::plugin_prefix( 'admin', '-' ), 63 \IQLRSS\Driver::get_asset_url( ' admin.css' ),58 \IQLRSS\Driver::get_asset_url( 'css/admin.css' ), 64 59 array(), 65 60 \IQLRSS\Driver::get( 'version', '1.0.0' ) … … 69 64 wp_register_script_module( 70 65 \IQLRSS\Driver::plugin_prefix( 'admin', '-' ), 71 \IQLRSS\Driver::get_asset_url( ' admin.js' ),66 \IQLRSS\Driver::get_asset_url( 'js/admin.js' ), 72 67 array( 'jquery' ), 73 68 \IQLRSS\Driver::get( 'version', '1.0.0' ) … … 93 88 $data = array( 94 89 'api_verified' => \IQLRSS\Driver::get_ss_opt( 'api_key_valid', false ), 95 'apiv1_verified'=> \IQLRSS\Driver::get_ss_opt( 'apiv1_key_valid', false ),96 90 'global_adjustment_type' => \IQLRSS\Driver::get_ss_opt( 'global_adjustment_type', '' ), 91 'store' => array( 92 'currency_symbol' => get_woocommerce_currency_symbol( get_woocommerce_currency() ), 93 ), 97 94 'rest' => array( 98 95 'nonce' => wp_create_nonce( 'wp_rest' ), 99 ' apiactions'=> get_rest_url( null, sprintf( '/%s/v1/apiactions',96 'settings'=> get_rest_url( null, sprintf( '/%s/v1/settings', 100 97 \IQLRSS\Driver::get( 'slug' ) 101 98 ) ), … … 105 102 'button_api_clearcache' => esc_html__( 'Clear API Cache', 'live-rates-for-shipstation' ), 106 103 'confirm_box_removal' => esc_html__( 'Please confirm you would like to completely remove (x) custom boxes.', 'live-rates-for-shipstation' ), 104 'confirm_modal_closure' => esc_html__( 'Changes you made may not be saved. Close modal window?', 'live-rates-for-shipstation' ), 105 'error_field_required' => esc_html__( 'This field is required, please enter a value.', 'live-rates-for-shipstation' ), 106 'error_custombox_json' => esc_html__( 'Something went wrong while saving your data. Please try again.', 'live-rates-for-shipstation' ), 107 107 'error_rest_generic' => esc_html__( 'Something went wrong with the REST Request. Please resave permalinks and try again.', 'live-rates-for-shipstation' ), 108 108 'error_verification_required' => esc_html__( 'Please click the Verify API button to ensure a connection exists.', 'live-rates-for-shipstation' ), 109 'success_custombox_added' => esc_html__( 'The Custom Box has been added to the list successfully!', 'live-rates-for-shipstation' ), 109 110 'desc_global_adjustment_percentage' => esc_html__( 'Example: IF UPS Ground is $7.25 and you input 15% ($1.08), the final shipping rate the customer sees is: $8.33', 'live-rates-for-shipstation' ), 110 111 'desc_global_adjustment_flatrate' => esc_html__( 'Example: IF UPS Ground is $5.50 and you input $2.37, the final shipping rate the customer sees is: $7.87', 'live-rates-for-shipstation' ), … … 196 197 197 198 /** 198 * REST Endpoint to validate the users API Key and clear API caches.199 *200 * @return void201 */202 public function api_actions_endpoint() {203 204 $prefix = \IQLRSS\Driver::get( 'slug' );205 206 // Handle ajax requests207 register_rest_route( "{$prefix}/v1", 'apiactions', array(208 'methods' => array( 'POST' ),209 'permission_callback' => fn() => is_user_logged_in(),210 'callback' => function( $request ) {211 212 $params = $request->get_params();213 if( ! isset( $params['action'] ) || empty( $params['action'] ) ) {214 wp_send_json_error();215 }216 217 switch( $params['action'] ) {218 219 // Clear the API Caches220 case 'clearcache':221 222 // Success!223 $this->clear_cache();224 wp_send_json_success();225 226 break;227 228 229 // Verify API Key230 case 'verify':231 232 // Error - Unknown Type233 if( empty( $params['type'] ) || ! in_array( $params['type'], array( 'v1', 'v2' ) ) ) {234 wp_send_json_error( esc_html__( 'System could not discern API type.', 'live-rates-for-shipstation' ), 401 );235 236 // Error - v1 API missing key or secret.237 } else if( 'v1' == $params['type'] && ( empty( $params['key'] ) || empty( $params['secret'] ) ) ) {238 wp_send_json_error( esc_html__( 'The ShipStation [v1] API required both a valid [v1] key and [v1] secret.', 'live-rates-for-shipstation' ), 401 );239 240 // Error v2 API missing api key.241 } else if( empty( $params['key'] ) ) {242 wp_send_json_error( esc_html__( 'The ShipStation v2 API requires an API key.', 'live-rates-for-shipstation' ), 401 );243 }244 245 $type = sanitize_title( $params['type'] );246 $settings = array(247 'v2' => \IQLRSS\Driver::get_ss_opt( 'api_key' ),248 'v2valid' => \IQLRSS\Driver::get_ss_opt( 'api_key_valid' ),249 'v2valid_time' => \IQLRSS\Driver::get_ss_opt( 'api_key_vt' ),250 'v1' => \IQLRSS\Driver::get_ss_opt( 'apiv1_key' ),251 'v1secret' => \IQLRSS\Driver::get_ss_opt( 'apiv1_secret' ),252 'v1valid' => \IQLRSS\Driver::get_ss_opt( 'apiv1_key_valid' ),253 'v1valid_time' => \IQLRSS\Driver::get_ss_opt( 'apiv1_key_vt' ),254 );255 $keydata = array(256 'old' => array(257 'key' => $settings[ $type ],258 'secret' => $settings['v1secret'],259 ),260 'new' => array(261 'key' => sanitize_text_field( $params['key'] ),262 'secret' => ( ! empty( $params['secret'] ) ) ? sanitize_text_field( $params['secret'] ) : '',263 )264 );265 266 // Only allow verification once a day if the data is the same.267 if( $keydata['old']['key'] == $keydata['new']['key'] ) {268 269 $valid_time = $settings["{$type}valid_time"];270 if( 'v1' == $type ) {271 $valid_time = ( $keydata['old']['secret'] != $keydata['new']['secret'] ) ? 0 : $valid_time;272 }273 274 // Return Early - We don't need to make a call, it is still valid.275 if( ! empty( $valid_time ) && $valid_time >= gmdate( 'Ymd', strtotime( 'today' ) ) ) {276 wp_send_json_success();277 }278 279 }280 281 // Verify the v1 API282 if( 'v1' == $type ) {283 284 // The API requires the keys to exist before being pinged.285 \IQLRSS\Driver::set_ss_opt( 'apiv1_key', $keydata['new']['key'] );286 \IQLRSS\Driver::set_ss_opt( 'apiv1_secret', $keydata['new']['secret'] );287 288 // Ping the stores so that it sets the currently connected store ID.289 $shipStationAPI = new Shipstation_Apiv1();290 $request = $shipStationAPI->get_stores();291 292 // Error - Something went wrong, the API should let us know.293 if( is_wp_error( $request ) || empty( $request ) ) {294 295 // Revert to old key and secret.296 \IQLRSS\Driver::set_ss_opt( 'apiv1_key', $keydata['old']['key'] );297 \IQLRSS\Driver::set_ss_opt( 'apiv1_secret', $keydata['old']['secret'] );298 299 $message = ( is_wp_error( $request ) ) ? $request->get_error_message() : '';300 $code = ( is_wp_error( $request ) ) ? $request->get_error_code() : 400;301 wp_send_json_error( $message, $code );302 303 }304 305 // Success! - Denote v2 validity and valid time.306 \IQLRSS\Driver::set_ss_opt( 'apiv1_key_valid', true );307 \IQLRSS\Driver::set_ss_opt( 'apiv1_key_vt', gmdate( 'Ymd', strtotime( 'today' ) ) );308 wp_send_json_success();309 310 // Verify the v2 API311 } else {312 313 // The API requires the keys to exist before being pinged.314 \IQLRSS\Driver::set_ss_opt( 'api_key', $keydata['new']['key'] );315 316 // Ping the carriers so that they are cached.317 $shipStationAPI = new Shipstation_Api();318 $request = $shipStationAPI->get_carriers();319 320 // Error - Something went wrong, the API should let us know.321 if( is_wp_error( $request ) || empty( $request ) ) {322 323 // Revert to old key.324 \IQLRSS\Driver::get_ss_opt( 'api_key', $keydata['old']['key'] );325 326 $message = ( is_wp_error( $request ) ) ? $request->get_error_message() : '';327 $code = ( is_wp_error( $request ) ) ? $request->get_error_code() : 400;328 wp_send_json_error( $message, $code );329 330 }331 332 // Success! - Denote v2 validity and valid time.333 \IQLRSS\Driver::set_ss_opt( 'api_key_valid', true );334 \IQLRSS\Driver::set_ss_opt( 'api_key_vt', gmdate( 'Ymd', strtotime( 'today' ) ) );335 wp_send_json_success();336 337 }338 339 break;340 }341 342 // Cases should return their own error/success.343 wp_send_json_error();344 }345 ) );346 347 }348 349 350 /**351 199 * Clear the API cache. 352 200 * … … 363 211 * The first WHERE ensures only `_transient_` and the 2nd ensures only our plugins transients. 364 212 */ 365 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s AND option_name LIKE %s", 213 $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE option_name LIKE %s AND option_name LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder 214 $wpdb->options, 366 215 $wpdb->esc_like( '_transient_' ) . '%', 367 216 '%' . $wpdb->esc_like( '_' . \IQLRSS\Driver::get( 'slug' ) . '_' ) . '%' … … 369 218 370 219 // Set transient to clear any WC_Session caches if they are found. 371 $expires = absint( apply_filters( 'wc_session_expiration', DAY_IN_SECONDS * 2 ) ); 220 $expires = absint( apply_filters( 'wc_session_expiration', DAY_IN_SECONDS * 2 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 372 221 set_transient( \IQLRSS\Driver::plugin_prefix( 'wcs_timeout' ), time(), $expires ); 373 222 … … 388 237 } 389 238 390 $this->clear_cache(); 391 392 } 393 394 395 /** 396 * Denote the exported order as a transient. 397 * Use the transient later to update the order via the v1 API. 398 * 399 * @param Integer $meta_id 400 * @param Integer $order_id 401 * @param String $meta_key 402 * @param String $meta_value 403 * 404 * @return void 405 */ 406 public function denote_shipstation_export( $meta_id, $order_id, $meta_key, $meta_value ) { 407 408 if( '_shipstation_exported' != $meta_key || 'yes' != $meta_value ) { 409 return; 410 } 411 412 $trans_key = \IQLRSS\Driver::plugin_prefix( 'exported_orders' ); 413 $order_ids = get_transient( $trans_key ); 414 $order_ids = ( ! empty( $order_ids ) ) ? $order_ids : array(); 415 416 // Return Early - Order ID already exists. 417 if( in_array( $order_id, $order_ids ) ) { 418 return; 419 } 420 421 $order_ids[] = $order_id; 422 set_transient( $trans_key, $order_ids, HOUR_IN_SECONDS ); 423 424 } 425 426 427 /** 428 * If an `_exported_orders` transient exists 429 * Update the order with some better info. 430 * 431 * @return void 432 */ 433 public function update_exported_orders() { 434 435 $trans_key = \IQLRSS\Driver::plugin_prefix( 'exported_orders' ); 436 $order_ids = get_transient( $trans_key ); 437 438 // Return Early - Delete transient, it's empty. 439 if( empty( $order_ids ) || ! is_array( $order_ids ) ) { 440 return delete_transient( $trans_key ); 441 } 442 443 // Grab the oldest order while also priming the WC_Order cache. 444 $wc_orders = wc_get_orders( array( 445 'include' => array_map( 'absint', $order_ids ), 446 'orderby' => 'date', 447 'order' => 'ASC', 448 'limit' => count( $order_ids ), 449 ) ); 450 451 // Return Early - Could't associate WC_Orders with transient order ids. 452 if( empty( $wc_orders ) ) { 453 return delete_transient( $trans_key ); 454 } 455 456 // Prime the cache 457 // API v1 will always cache it's ShipStation data in the WC_Order as metadata. 458 $apiv1 = new Shipstation_Apiv1( true ); 459 $apiv1->get_orders( array( 460 'createDateEnd' => gmdate( 'c', time() ), 461 ) ); 462 463 $api = new Shipstation_Api( true ); 464 $api->create_shipments_from_wc_orders( $wc_orders ); 465 466 return delete_transient( $trans_key ); 239 \IQLRSS\Driver::clear_cache(); 467 240 468 241 } … … 514 287 public function append_shipstation_integration_settings( $fields ) { 515 288 516 $carriers = array(); 289 $carriers = array( 290 '' => esc_html__( 'ShipStation carriers may still be loading...', 'live-rates-for-shipstation' ), 291 ); 517 292 $appended_fields = array(); 518 293 … … 520 295 521 296 $carrier_desc = esc_html__( 'Select which ShipStation carriers you would like to see live shipping rates from.', 'live-rates-for-shipstation' ); 522 $ shipStationAPI = new Shipstation_Api();523 $response = $shipStationAPI->get_carriers(); 524 297 $response = ( new Api\Shipstation() )->get_carriers(); 298 299 $carriers = array(); 525 300 if( is_a( $response, 'WP_Error' ) ) { 526 301 $carriers[''] = $response->get_error_message(); … … 552 327 'default' => '', 553 328 ); 554 555 // $appended_fields[ \IQLRSS\Driver::plugin_prefix( 'apiv1_key' ) ] = array(556 // 'title' => esc_html__( 'ShipStation [v1] API Key', 'live-rates-for-shipstation' ),557 // 'type' => 'password',558 // 'description' => esc_html__( 'See "ShipStation REST API Key" description, but instead of selecting [v2], select [v1].', 'live-rates-for-shipstation' ),559 // 'default' => '',560 // );561 562 // $appended_fields[ \IQLRSS\Driver::plugin_prefix( 'apiv1_secret' ) ] = array(563 // 'title' => esc_html__( 'ShipStation [v1] API Secret', 'live-rates-for-shipstation' ),564 // 'type' => 'password',565 // 'description' => esc_html__( 'The v1 API is _required_ to manage orders. The v2 API handles Live Rates.', 'live-rates-for-shipstation' ),566 // 'default' => '',567 // );568 329 569 330 $appended_fields[ \IQLRSS\Driver::plugin_prefix( 'carriers' ) ] = array( … … 643 404 } 644 405 645 $this->clear_cache(); 646 } 647 648 // No [v1] API Key? Invalid! 649 $apiv1_key_key = \IQLRSS\Driver::plugin_prefix( 'apiv1_key' ); 650 if( ! isset( $settings[ $apiv1_key_key ] ) || empty( $settings[ $apiv1_key_key ] ) ) { 651 652 $settings[ \IQLRSS\Driver::plugin_prefix( 'apiv1_key_valid' ) ] = false; 653 if( isset( $settings[ \IQLRSS\Driver::plugin_prefix( 'apiv1_key_vt' ) ] ) ) { 654 unset( $settings[ \IQLRSS\Driver::plugin_prefix( 'apiv1_key_vt' ) ] ); 655 } 656 657 $this->clear_cache(); 406 \IQLRSS\Driver::clear_cache(); 658 407 } 659 408 … … 745 494 746 495 // Integration > ShipStation settings page 747 $enqueue = ( $enqueue || isset( $_GET, $_GET['section'] ) && 'shipstation' == $_GET['section']); // phpcs:ignore WordPress.Security.NonceVerification.Recommended496 $enqueue = ( $enqueue || ( isset( $_GET, $_GET['section'] ) && 'shipstation' == $_GET['section'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended 748 497 749 498 // Overprotective WooCommerce settings page check -
live-rates-for-shipstation/trunk/core/shipping-method-shipstation.php
r3376459 r3407166 2 2 /** 3 3 * ShipStation Live Shipping Rates Method 4 * 5 * @todo Consider moving Shipping Calculations into it's own class. 6 * 7 * @link https://www.fedex.com/en-us/shipping/one-rate.html 8 * @link https://www.usps.com/ship/priority-mail.htm#flatrate 9 * @link https://www.ups.com/worldshiphelp/WSA/ENG/AppHelp/mergedProjects/CORE/Codes/Package_Type_Codes.htm 4 10 * 5 11 * :: Action Hooks … … 28 34 29 35 /** 30 * Array of expected dimension keys (width, height, length, weight)31 *32 * @var Array33 */34 protected $dimension_keys = array(35 'width' => 'width',36 'height' => 'height',37 'length' => 'length',38 'weight' => 'weight',39 );40 41 42 /**43 36 * Array of store specific settings. 44 37 * … … 86 79 87 80 $this->plugin_prefix = \IQLRSS\Driver::get( 'slug' ); 88 $this->shipStationApi = new Shipstation_Api();81 $this->shipStationApi = new Api\Shipstation(); 89 82 $this->id = \IQLRSS\Driver::plugin_prefix( 'shipstation' ); 90 83 $this->instance_id = absint( $instance_id ); … … 133 126 */ 134 127 private function action_hooks() { 128 135 129 add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); 130 add_action( 'admin_footer', array( $this, 'hide_zone_setting_fields' ) ); 131 136 132 } 137 133 … … 146 142 ( new \IQLRSS\Core\Settings_Shipstation() )->clear_cache(); 147 143 return parent::process_admin_options(); 144 145 } 146 147 148 /** 149 * Hide Shipping Zone setting fields 150 * 1). Since they're row options we rarely have markup control over. 151 * 2). Since modules load JS a bit later. 152 * 153 * @return void 154 */ 155 public function hide_zone_setting_fields() { 156 157 ?><script type="text/javascript"> 158 159 /* Hide onebox when not set */ 160 if( document.getElementById( 'woocommerce_iqlrss_shipstation_packing' ) ) { ( function() { 161 if( 'onebox' != document.getElementById( 'woocommerce_iqlrss_shipstation_packing' ).value ) { 162 document.getElementById( 'woocommerce_iqlrss_shipstation_packing' ).closest( 'tr' ).nextElementSibling.style.display = 'none'; 163 } 164 } )(); } 165 </script><?php 148 166 149 167 } … … 293 311 if( isset( $rate_arr['other_costs'] ) ) { 294 312 foreach( $rate_arr['other_costs'] as $o_slug => $o_amount ) { 295 $new_display .= sprintf( ' | %s: %s', ucwords( $o_slug), wc_price( $o_amount ) );313 $new_display .= sprintf( ' | %s: %s', ucwords( str_replace( array( '-', '_' ), ' ', $o_slug ) ), wc_price( $o_amount ) ); 296 314 } 297 315 } … … 314 332 $display_arr = array(); 315 333 foreach( $value as $i => $box_arr ) { 334 335 /* translators: %1$d is box/package count (1,2,3). */ 336 $box_name = sprintf( esc_html__( 'Package %1$d', 'live-rates-for-shipstation' ), $i + 1 ); 337 if( ! empty( $box_arr['nickname'] ) ) { 338 $box_name = $box_arr['nickname']; 339 } 316 340 317 341 $names = esc_html__( 'Product', 'live-rates-for-shipstation' ); … … 323 347 }, $box_arr['packed'] ); 324 348 } 325 326 349 $display_arr[] = sprintf( '%s ( %s ) [ %s %s ( %s x %s x %s %s ) ]', 327 328 /* translators: %1$d is box/package count (1,2,3). */ 329 sprintf( esc_html__( 'Package %1$d', 'live-rates-for-shipstation' ), $i + 1 ), 350 $box_name, 330 351 implode( ', ', (array)$names ), 331 352 $box_arr['weight']['value'], … … 388 409 protected function init_instance_form_fields() { 389 410 390 $ this->instance_form_fields = array(411 $settings = array( 391 412 'title' => array( 392 413 'title' => esc_html__( 'Title', 'live-rates-for-shipstation' ), … … 395 416 'default' => esc_html__( 'ShipStation Rates', 'live-rates-for-shipstation' ), 396 417 'desc_tip' => true, 418 ), 419 'minweight' => array( 420 'title' => esc_html__( 'Product Weight Fallback', 'live-rates-for-shipstation' ), 421 'type' => 'text', 422 'description' => esc_html__( 'This value will be used if both weight and dimensions are missing from any given product. ShipStation at minimum needs a product weight to retrieve rates.', 'live-rates-for-shipstation' ), 397 423 ), 398 424 'packing' => array( … … 403 429 'individual' => esc_html__( 'Pack items individually', 'live-rates-for-shipstation' ), 404 430 'wc-box-packer' => esc_html__( 'Pack items using Custom Packing Boxes', 'live-rates-for-shipstation' ), 431 'onebox' => esc_html__( 'Pack items into one package derived from products', 'live-rates-for-shipstation' ), 405 432 ), 406 433 'description' => esc_html__( 'Individually can be more costly. Custom packing boxes will automatically fit as many products in set dimensions lowering shipping costs.', 'live-rates-for-shipstation' ), 434 ), 435 'packing_sub' => array( 436 'title' => esc_html__( 'Package Dimensions', 'live-rates-for-shipstation' ), 437 'type' => 'select', 438 'options' => array( 439 'weightonly' => esc_html__( 'Total weight', 'live-rates-for-shipstation' ), 440 'stacked' => esc_html__( 'Stacked vertically', 'live-rates-for-shipstation' ), 441 ), 442 'description' => esc_html__( 'Stacked vertically - sums product heights and weights, takes largest of other dimensions. Weight only sums product weights and retrieves rates using the total.', 'live-rates-for-shipstation' ), 407 443 ), 408 444 'customboxes' => array( … … 414 450 ); 415 451 452 453 /** 454 * Allow filtering the Shipping Zone settings 455 * 456 * @hook filter 457 * 458 * @param Array $settings 459 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 460 * 461 * @return Array $settings 462 */ 463 $settings = apply_filters( 'iqlrss/zone/settings', $settings, $this ); 464 $this->instance_form_fields = $settings; 465 466 } 467 468 469 /** 470 * Automatic dynamic method inherited from parent. 471 * Generate HTML for custom boxes fields. 472 * 473 * @return String - HTML 474 */ 475 public function generate_customboxes_html() { 476 477 $prefix = $this->plugin_prefix; 478 $show_custom = ( 'wc-box-packer' == $this->get_option( 'packing', 'individual' ) ); 479 $saved_boxes = $this->get_option( 'customboxes', array() ); 480 $packages = $this->get_package_options(); 481 482 ob_start(); 483 include 'assets/views/customboxes-table.php'; 484 return ob_get_clean(); 485 486 } 487 488 489 /** 490 * Validate customboxes. 491 * 492 * @return Array $boxes 493 */ 494 public function validate_customboxes_field() { 495 496 if( ! isset( $_POST['_wpnonce'] ) ) { 497 return; 498 } 499 500 $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ); 501 if( ! wp_verify_nonce( $nonce, 'woocommerce-settings' ) ) { 502 return; 503 } else if( ! isset( $_POST['custombox'] ) || ! is_array( $_POST['custombox'] ) ) { 504 return; 505 } 506 507 // Input sanitized during processing. 508 $posted_boxes = wp_unslash( $_POST['custombox'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 509 510 $boxes = array(); 511 foreach( $posted_boxes as $box_arr ) { 512 513 if( isset( $box_arr['json'] ) ) { 514 515 $json = json_decode( $box_arr['json'], true ); 516 if( empty( $json['outer'] ) ) continue; 517 518 $boxes[] = array( 519 'active' => absint( $json['active'] ), 520 'preset' => ( isset( $json['preset'] ) ) ? sanitize_text_field( $json['preset'] ) : '', 521 'nickname' => sanitize_text_field( $json['nickname'] ), 522 'outer' => array( 523 'length' => floatval( $json['outer']['length'] ), 524 'width' => floatval( $json['outer']['width'] ), 525 'height' => floatval( $json['outer']['height'] ), 526 ), 527 'inner' => array( 528 'length' => floatval( $json['inner']['length'] ), 529 'width' => floatval( $json['inner']['width'] ), 530 'height' => floatval( $json['inner']['height'] ), 531 ), 532 'weight' => floatval( $json['weight'] ), 533 'weight_max'=> floatval( $json['weight_max'] ), 534 'price' => floatval( $json['price'] ), 535 'carrier_code' => ( isset( $json['carrier_code'] ) ) ? sanitize_text_field( $json['carrier_code'] ) : '', 536 ); 537 538 } 539 } 540 541 usort( $boxes, function( $arrA, $arrB ) { 542 return strcasecmp( $arrA['nickname'], $arrB['nickname'] ); 543 } ); 544 545 return $boxes; 546 416 547 } 417 548 … … 459 590 460 591 ob_start(); 461 include 'views/services-table.php'; 462 return ob_get_clean(); 463 464 } 465 466 467 /** 468 * Automatic dynamic method inherited from parent. 469 * Generate HTML for custom boxes fields. 470 * 471 * @return String - HTML 472 */ 473 public function generate_customboxes_html() { 474 475 $prefix = $this->plugin_prefix; 476 $show_custom = ( 'wc-box-packer' == $this->get_option( 'packing', 'individual' ) ); 477 $saved_boxes = $this->get_option( 'customboxes', array() ); 478 479 ob_start(); 480 include 'views/customboxes-table.php'; 592 include 'assets/views/services-table.php'; 481 593 return ob_get_clean(); 482 594 … … 563 675 564 676 565 /**566 * Validate customboxes field.567 *568 * @return Array $boxes569 */570 public function validate_customboxes_field() {571 572 if( ! isset( $_POST['_wpnonce'] ) ) {573 return;574 }575 576 $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) );577 if( ! wp_verify_nonce( $nonce, 'woocommerce-settings' ) ) {578 return;579 } else if( ! isset( $_POST['custombox'] ) || ! is_array( $_POST['custombox'] ) ) {580 return;581 }582 583 // Input sanitized during processing.584 $posted_boxes = wp_unslash( $_POST['custombox'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized585 586 $boxes = array();587 foreach( $posted_boxes as $box_arr ) {588 589 $vals = array_filter( $box_arr, 'is_numeric' );590 if( count( $vals ) < 7 ) continue;591 592 $boxes[] = array(593 'outer' => array(594 'length' => floatval( $box_arr['ol'] ),595 'width' => floatval( $box_arr['ow'] ),596 'height' => floatval( $box_arr['oh'] ),597 ),598 'inner' => array(599 'length' => floatval( $box_arr['il'] ),600 'width' => floatval( $box_arr['iw'] ),601 'height' => floatval( $box_arr['ih'] ),602 ),603 'weight' => floatval( $box_arr['w'] ),604 'weight_max'=> floatval( $box_arr['wm'] ),605 );606 607 }608 609 return $boxes;610 611 }612 613 614 677 615 678 /**------------------------------------------------------------------------------------------------ **/ … … 619 682 * Calculate shipping costs 620 683 * 621 * @param Array $package 684 * @param Array $packages 622 685 * 623 686 * @return void … … 631 694 // Try to pull from cache. This may set $this->rates 632 695 // Return Early - We have cached rates to work with! 633 $ packages_hash = $this->check_packages_rate_cache( $packages );696 $this->check_packages_rate_cache( $packages ); 634 697 if( ! empty( $this->rates ) ) { 635 698 return; … … 642 705 $enabled_services = $this->get_enabled_services(); 643 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' ) ); 644 708 return; 645 709 } … … 669 733 ); 670 734 671 // Individual Packaging 672 if( 'individual' == $packing_type ) { 673 $item_requests = $this->get_individual_requests( $packages['contents'] ); 674 675 // WC Boxed Packaging 676 } else { 677 $item_requests = $this->get_custombox_requests( $packages['contents'] ); 678 } 679 680 // Rates groups shipping estimates by service ID. 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 } 740 741 742 /** 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 * 772 * @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. 776 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 777 * 778 * @return Array $settings 779 */ 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 */ 681 794 $rates = array(); 682 683 /** 684 * This has to be done per package as the other rates endpoint 685 * requires the customers address1 for verification and really 686 * it's not much faster. 687 */ 688 foreach( $item_requests as $item_id => $req ) { 795 foreach( $filtered_requests as $item_id => $req ) { 689 796 690 797 // Create the API request combining the package (weight, dimensions), general request data, and the carrier info. … … 700 807 // Continue - Something went wrong, should be logged on the API side. 701 808 $available_rates = $this->shipStationApi->get_shipping_estimates( $api_request ); 809 702 810 if( is_wp_error( $available_rates ) || empty( $available_rates ) ) { 703 811 continue; … … 711 819 } 712 820 821 $ratehash = md5( sprintf( '%s%s', $shiprate['code'], $shiprate['carrier_id'] ) ); 713 822 $service_arr = $enabled_services[ $shiprate['carrier_id'] ][ $shiprate['code'] ]; 714 $cost = floatval( $shiprate['cost'] ); 715 $ratemeta = array( 716 '_name'=> ( isset( $req['_name'] ) ) ? $req['_name'] : '', // Item product name. 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. 717 828 'rate' => $cost, 718 829 ); … … 767 878 } 768 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 769 886 // Maybe apply per item. 770 887 if( 'individual' == $packing_type ) { … … 774 891 775 892 // Set rate or append the estimated item ship cost. 776 if( ! isset( $rates[ $ shiprate['code']] ) ) {777 778 $rates[ $ shiprate['code']] = array(779 'id' => $ shiprate['code'],893 if( ! isset( $rates[ $ratehash ] ) ) { 894 895 $rates[ $ratehash ] = array( 896 'id' => $ratehash, 780 897 'label' => ( ! empty( $service_arr['nickname'] ) ) ? $service_arr['nickname'] : $shiprate['name'], 781 898 'package' => $packages, … … 795 912 796 913 } else { 797 $rates[ $ shiprate['code']]['cost'][] = $cost;914 $rates[ $ratehash ]['cost'][] = $cost; 798 915 } 799 916 800 917 // Merge item rates 801 $rates[ $ shiprate['code']]['meta_data']['rates'] = array_merge(802 $rates[ $ shiprate['code']]['meta_data']['rates'],918 $rates[ $ratehash ]['meta_data']['rates'] = array_merge( 919 $rates[ $ratehash ]['meta_data']['rates'], 803 920 array( $ratemeta ), 804 921 ); 805 922 806 923 // Merge item boxes 807 $rates[ $ shiprate['code']]['meta_data']['boxes'] = array_merge(808 $rates[ $ shiprate['code']]['meta_data']['boxes'],924 $rates[ $ratehash ]['meta_data']['boxes'] = array_merge( 925 $rates[ $ratehash ]['meta_data']['boxes'], 809 926 array( $req ), 810 927 ); … … 814 931 } 815 932 816 $single_lowest = \IQLRSS\Driver::get_ss_opt( 'return_lowest', 'no' );817 $single_lowest_label = \IQLRSS\Driver::get_ss_opt( 'return_lowest_label', '' );933 $single_lowest = \IQLRSS\Driver::get_ss_opt( 'return_lowest', 'no' ); 934 $single_lowest_label = \IQLRSS\Driver::get_ss_opt( 'return_lowest_label', '' ); 818 935 819 936 // Add all shipping rates, let the user decide. … … 822 939 foreach( $rates as $rate_arr ) { 823 940 824 // Skip incomplete rate requests 825 if( count( $item_requests ) != count( $rate_arr['cost'] ) ) { 826 continue; 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'] ); 827 945 } 828 946 829 947 // WooCommerce skips serialized data when outputting order item meta, this is a workaround. 830 948 // See hooks above for formatting. 831 $rate_arr['meta_data']['rates'] = json_encode( $rate_arr['meta_data']['rates'] );832 $rate_arr['meta_data']['boxes'] = json_encode( $rate_arr['meta_data']['boxes'] );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'] ); 833 951 834 952 $this->add_rate( $rate_arr ); … … 855 973 // WooCommerce skips serialized data when outputting order item meta, this is a workaround. 856 974 // See hooks above for formatting. 857 $rates[ $lowest_service ]['meta_data']['rates'] = json_encode( $rates[ $lowest_service ]['meta_data']['rates'] );858 $rates[ $lowest_service ]['meta_data']['boxes'] = json_encode( $rates[ $lowest_service ]['meta_data']['boxes'] );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'] ); 859 977 860 978 $this->add_rate( $rates[ $lowest_service ] ); … … 862 980 } 863 981 864 // Add a cache key to check against. 865 WC()->session->set( $this->plugin_prefix, array_merge( 866 WC()->session->get( $this->plugin_prefix, array() ), 867 array( 'method_hash' => $packages_hash ), 982 $cachehash = $this->generate_packages_cache_key( $packages ); 983 if( empty( $cachehash ) ) return; 984 985 // Cache packages to prevent multiple requests. 986 WC()->session->set( $this->plugin_prefix . '_packages', array_merge( 987 WC()->session->get( $this->plugin_prefix . '_packages', array() ), 988 array( 'method_hash' => $cachehash ), 868 989 array( 'method_cache_time' => time() ), 869 990 ) ); … … 879 1000 * @return Array $requests 880 1001 */ 881 protected function get_individual_requests( $items ) { 882 883 $item_requests = array(); 1002 public function group_requestsby_individual( $items ) { 1003 1004 $item_requests = array(); 1005 $default_weight = $this->get_option( 'minweight', '' ); 1006 884 1007 foreach( $items as $item_id => $item ) { 885 1008 … … 894 1017 $item['data']->get_name(), 895 1018 ), 1019 'weight' => ( ! empty( $item['data']->get_weight() ) ) ? $item['data']->get_weight() : $default_weight, 896 1020 ); 897 1021 $physicals = array_filter( array( 898 'weight' => $item['data']->get_weight(),899 1022 'length' => $item['data']->get_length(), 900 1023 'width' => $item['data']->get_width(), … … 903 1026 904 1027 // Return Early - Product missing one of the 4 key dimensions. 905 if( count( $physicals ) < 4) {1028 if( count( $physicals ) < 3 || empty( $request['weight'] ) ) { 906 1029 $this->log( sprintf( 907 1030 908 1031 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */ 909 esc_html__( 'Product ID #%1$d missing (%2$s) dimensions. Shipping calculations terminated.', 'live-rates-for-shipstation' ), 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 Box 1075 * Group all the products by weight and get rates by total weight. 1076 * 1077 * @param Array $items 1078 * 1079 * @return Array $requests 1080 */ 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 Verticially 1169 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 // Largest 1178 '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 // Running 1182 '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 $items 1194 * 1195 * @return Array $requests 1196 */ 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' ), 910 1240 $item['product_id'], 911 1241 implode( ', ', array_diff_key( array( … … 913 1243 'height' => 'height', 914 1244 'length' => 'length', 915 'weight' => 'weight',916 1245 ), $physicals ) ) 917 1246 ) ); … … 919 1248 } 920 1249 921 $request['weight'] = array(922 'value' => (float)round( wc_get_weight( $physicals['weight'], $this->store_data['weight_unit'] ), 2 ),923 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ),924 );925 926 // Unset weight and sort dimensions927 unset( $physicals['weight'] );928 1250 sort( $physicals ); 929 930 $request['dimensions'] = array(931 'length' => round( wc_get_dimension( $physicals[2], $this->store_data['dim_unit'] ), 2 ),932 'width' => round( wc_get_dimension( $physicals[1], $this->store_data['dim_unit'] ), 2 ),933 'height' => round( wc_get_dimension( $physicals[0], $this->store_data['dim_unit'] ), 2 ),934 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ),935 );936 937 $item_requests[ $item_id ] = $request;938 939 }940 941 return $item_requests;942 943 }944 945 946 /**947 * Return an array of API requests for custom packed boxes.948 * Shoutout to Mike Jolly & Co.949 *950 * @param Array $items951 *952 * @return Array $requests953 */954 protected function get_custombox_requests( $items ) {955 956 if( ! class_exists( '\IQRLSS\WC_Box_Packer\WC_Boxpack' ) ) {957 include_once 'wc-box-packer/class-wc-boxpack.php';958 }959 960 $item_requests = array();961 $wc_boxpack = new WC_Box_Packer\WC_Boxpack();962 $boxes = $this->get_option( 'customboxes', array() );963 964 if( empty( $boxes ) ) {965 $this->log( esc_html__( 'Custom Boxes selected, but no boxes found. Items packed individually', 'live-rates-for-shipstation' ), 'warning' );966 }967 968 // Setup the WC_Boxpack boxes based on user submitted custom boxes.969 foreach( $boxes as $box ) {970 971 $custombox = $wc_boxpack->add_box( $box['outer']['length'], $box['outer']['width'], $box['outer']['height'], $box['weight'] );972 $custombox->set_inner_dimensions( $box['inner']['length'], $box['inner']['width'], $box['inner']['height'] );973 if( $box['weight_max'] ) $custombox->set_max_weight( $box['weight_max'] );974 975 }976 977 // Loop the items, grabs their dimensions, and assocaite them with WC_Boxpack for future packing.978 foreach( $items as $item_id => $item ) {979 980 // Continue - No shipping needed for product.981 if( ! $item['data']->needs_shipping() ) {982 continue;983 }984 985 $data = array();986 $physicals = array_filter( array(987 'weight' => $item['data']->get_weight(),988 'length' => $item['data']->get_length(),989 'width' => $item['data']->get_width(),990 'height' => $item['data']->get_height(),991 ) );992 993 // Return Early - Product missing one of the 4 key dimensions.994 if( count( $physicals ) < 4 ) {995 $this->log( sprintf(996 997 /* translators: %1$d is the Product ID. %2$s is the Product Dimensions separated by a comma. */998 esc_html__( 'Product ID #%1$d missing (%2$s) dimensions. Shipping calculations terminated.', 'live-rates-for-shipstation' ),999 $item['product_id'],1000 implode( ', ', array_diff_key( array(1001 'width' => 'width',1002 'height' => 'height',1003 'length' => 'length',1004 'weight' => 'weight',1005 ), $physicals ) )1006 ) );1007 return array();1008 }1009 1010 $data['weight'] = (float)round( wc_get_weight( $physicals['weight'], $this->store_data['weight_unit'] ), 2 );1011 1012 // Unset weight to exclude it from sort1013 unset( $physicals['weight'] );1014 sort( $physicals );1015 1016 1251 $data = array( 1017 1252 'length' => round( wc_get_dimension( $physicals[2], $this->store_data['dim_unit'] ), 2 ), 1018 1253 'width' => round( wc_get_dimension( $physicals[1], $this->store_data['dim_unit'] ), 2 ), 1019 1254 'height' => round( wc_get_dimension( $physicals[0], $this->store_data['dim_unit'] ), 2 ), 1020 ) + $data; 1021 1255 'weight' => round( wc_get_weight( $data['weight'], $this->store_data['weight_unit'] ), 2 ), 1256 ); 1257 1258 // Pack Products 1022 1259 for( $i = 0; $i < $item['quantity']; $i++ ) { 1023 1260 $wc_boxpack->add_item( … … 1048 1285 $item_requests[] = array( 1049 1286 'weight' => array( 1050 'value' => $package->weight,1287 'value' => round( $package->weight, 2 ), 1051 1288 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['weight_unit'] ), 1052 1289 ), 1053 1290 'dimensions' => array( 1054 'length' => $package->length,1055 'width' => $package->width,1056 'height' => $package->height,1291 'length' => round( $package->length, 2 ), 1292 'width' => round( $package->width, 2 ), 1293 'height' => round( $package->height, 2 ), 1057 1294 'unit' => $this->shipStationApi->convert_unit_term( $this->store_data['dim_unit'] ), 1058 1295 ), 1059 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'] : '', 1060 1303 ); 1061 1304 … … 1073 1316 ), 1074 1317 'max_volume' => floatval( $package->width * $package->height * $package->length ), 1318 'data' => ( ! empty( $package->data ) ) ? $package->data : array(), 1075 1319 ); 1076 1320 … … 1087 1331 1088 1332 /** 1089 * Attempt to pull from the WC() Session cache to prevent multiple caclulation 1333 * Set the rates based on cached packages. 1334 * 1335 * Attempt to pull from the WC() Session cache to prevent multiple calculations 1090 1336 * requests, which could unnecessarily ping the API or add duplicate logs. 1091 1337 * This issue is common when dealing with WP Blocks/Gutenberg Editor. … … 1093 1339 * @param Array $packages - Packages in use. 1094 1340 * 1095 * @return String $hash - hash key neded to reset cache.1341 * @return void 1096 1342 */ 1097 1343 protected function check_packages_rate_cache( $packages ) { 1098 1344 1099 $session = WC()->session->get( $this->plugin_prefix , array() );1345 $session = WC()->session->get( $this->plugin_prefix . '_packages', array() ); 1100 1346 $cleartime = get_transient( \IQLRSS\Driver::plugin_prefix( 'wcs_timeout' ) ); 1347 $cachehash = $this->generate_packages_cache_key( $packages ); 1348 1349 // Return Early - Cache cleared or 30 minuites has passed (invalidate cache). 1350 if( isset( $session['method_cache_time'] ) && ( $cleartime > $session['method_cache_time'] || $session['method_cache_time'] < ( time() - ( 30 * 60 ) ) ) ) { 1351 return; 1352 1353 // Return Early- Cart has changed. 1354 } else if( ! isset( $session['method_hash'] ) || empty( $cachehash ) || $session['method_hash'] != $cachehash ) { 1355 return; 1356 } 1357 1358 // Try to populate Rates. 1359 $size = count( $packages ); 1360 for( $i = 0; $i < $size; $i++ ) { 1361 1362 $cache = WC()->session->get( 'shipping_for_package_' . $i, false ); 1363 if( empty( $cache ) || ! is_array( $cache ) ) { 1364 break; 1365 } 1366 $this->rates = array_merge( $cache['rates'], $this->rates ); 1367 1368 } 1369 1370 } 1371 1372 1373 /** 1374 * Generate a hash key based off of the given packages. 1375 * 1376 * @param Array $packages 1377 * 1378 * @return String $hash 1379 */ 1380 protected function generate_packages_cache_key( $packages ) { 1101 1381 1102 1382 $keys = array(); … … 1108 1388 ); 1109 1389 } 1110 $hash = md5( wp_json_encode( $keys ) ) . \WC_Cache_Helper::get_transient_version( 'shipping' ); 1111 1112 // Return Early - Cache cleared or 30 minuites has passed (invalidate cache). 1113 if( isset( $session['method_cache_time'] ) && ( $cleartime > $session['method_cache_time'] || $session['method_cache_time'] < ( time() - ( 30 * 60 ) ) ) ) { 1114 return $hash; 1115 1116 // Return Early- Cart has changed. 1117 } else if( ! isset( $session['method_hash'] ) || $session['method_hash'] != $hash ) { 1118 return $hash; 1119 } 1120 1121 // Try to populate Rates. 1122 $size = count( $packages ); 1123 for( $i = 0; $i < $size; $i++ ) { 1124 1125 $cache = WC()->session->get( 'shipping_for_package_' . $i, false ); 1126 if( empty( $cache ) || ! is_array( $cache ) ) { 1127 break; 1128 } 1129 $this->rates = array_merge( $cache['rates'], $this->rates ); 1130 1131 } 1132 1133 return $hash; 1134 1135 } 1136 1137 1138 /** 1139 * Generate a hash key based off of the given packages. 1140 * 1141 * @param Array $packages 1142 * 1143 * @return String $hash 1144 */ 1145 protected function generate_packages_cache_key( $packages ) { 1146 1147 // Maybe skip if cache was cleared. 1148 $session = WC()->session->get( $this->plugin_prefix, array() ); 1149 $cleartime = get_transient( \IQLRSS\Driver::plugin_prefix( 'wcs_timeout' ) ); 1150 if( isset( $session['method_cache_time'] ) && $cleartime > $session['method_cache_time'] ) { 1151 return ''; 1152 } 1153 1154 $keys = array(); 1155 foreach( $packages['contents'] as $key => $package ) { 1156 $keys[] = array( 1157 $key, 1158 $package['quantity'], 1159 $package['line_total'], 1160 ); 1161 } 1162 1163 $hash = md5( wp_json_encode( $keys ) ) . \WC_Cache_Helper::get_transient_version( 'shipping' ); 1164 return ( ! empty( $keys ) ) ? $hash : ''; 1390 1391 if( empty( $keys ) ) return ''; 1392 return md5( wp_json_encode( $keys ) ) . \WC_Cache_Helper::get_transient_version( 'shipping' ); 1165 1393 1166 1394 } … … 1171 1399 /** :: Helper Methods :: **/ 1172 1400 /**------------------------------------------------------------------------------------------------ **/ 1401 /** 1402 * Map known packages. 1403 * @see assets/json 1404 * 1405 * @param String $key 1406 * 1407 * @return String 1408 */ 1409 public function get_package_label( $key ) { 1410 1411 $labels = array( 1412 // UPS 1413 'flat_rate_envelope' => esc_html__( 'USPS Flat Rate Envelope', 'live-rates-for-shipstation' ), 1414 'flat_rate_legal_envelope' => esc_html__( 'USPS Flat Rate Legal Envelope', 'live-rates-for-shipstation' ), 1415 'flat_rate_padded_envelope' => esc_html__( 'USPS Flat Rate Padded Envelope', 'live-rates-for-shipstation' ), 1416 'large_envelope_or_flat'=> esc_html__( 'USPS Large Envelope or Flat', 'live-rates-for-shipstation' ), 1417 'large_flat_rate_box' => esc_html__( 'USPS Large Flat Rate Box', 'live-rates-for-shipstation' ), 1418 'medium_flat_rate_box' => esc_html__( 'USPS Medium Flat Rate Box', 'live-rates-for-shipstation' ), 1419 'small_flat_rate_box' => esc_html__( 'USPS Small Flat Rate Box', 'live-rates-for-shipstation' ), 1420 'regional_rate_box_a' => esc_html__( 'USPS Regional Rate Box A', 'live-rates-for-shipstation' ), 1421 'regional_rate_box_b' => esc_html__( 'USPS Regional Rate Box B', 'live-rates-for-shipstation' ), 1422 1423 // USPS 1424 'ups_10_kg_box' => esc_html__( 'UPS 10kg (22lbs) Box', 'live-rates-for-shipstation' ), 1425 'ups_25_kg_box' => esc_html__( 'UPS 25kg (55lbs) Box', 'live-rates-for-shipstation' ), 1426 'ups__express_box_large'=> esc_html__( 'UPS Express Box - Large', 'live-rates-for-shipstation' ), // Why does this have an extra underscore? Ask ShipStation. 1427 'ups_express_box_medium'=> esc_html__( 'UPS Express Box - Medium', 'live-rates-for-shipstation' ), 1428 'ups_express_box_small' => esc_html__( 'UPS Express Box - Small', 'live-rates-for-shipstation' ), 1429 'ups_tube' => esc_html__( 'UPS Tube', 'live-rates-for-shipstation' ), 1430 'ups_express_pak' => esc_html__( 'UPS Express Pak', 'live-rates-for-shipstation' ), 1431 'ups_letter' => esc_html__( 'UPS Letter', 'live-rates-for-shipstation' ), 1432 1433 // FedEx 1434 'fedex_10kg_box' => esc_html__( 'FedEx 10kg (22lbs) Box', 'live-rates-for-shipstation' ), 1435 'fedex_25kg_box' => esc_html__( 'FedEx 25kg (55lbs) Box', 'live-rates-for-shipstation' ), 1436 'fedex_extra_large_box' => esc_html__( 'FedEx Extra Large Box', 'live-rates-for-shipstation' ), 1437 'fedex_large_box' => esc_html__( 'FedEx Large Box', 'live-rates-for-shipstation' ), 1438 'fedex_medium_box' => esc_html__( 'FedEx Medium Box', 'live-rates-for-shipstation' ), 1439 'fedex_small_box' => esc_html__( 'FedEx Small Box', 'live-rates-for-shipstation' ), 1440 'fedex_tube' => esc_html__( 'FedEx Tube', 'live-rates-for-shipstation' ), 1441 'fedex_envelope' => esc_html__( 'FedEx Envelope', 'live-rates-for-shipstation' ), 1442 'fedex_pak' => esc_html__( 'FedEx Padded Pak', 'live-rates-for-shipstation' ), 1443 ); 1444 1445 return ( isset( $labels [ $key ] ) ) ? $labels[ $key ] : esc_html__( 'Unknown Package', 'live-rates-for-shipstation' ); 1446 1447 } 1448 1449 1173 1450 /** 1174 1451 * Return an array of Price Adjustment Type options. … … 1214 1491 1215 1492 /** 1493 * Convert a WooCommerce unit to a ShipStation unit. 1494 * 1495 * @param String $unit 1496 * 1497 * @return String $new_unit 1498 */ 1499 public function convert_unit_term( $unit ) { 1500 return $this->shipStationApi->convert_unit_term( $unit ); 1501 } 1502 1503 1504 /** 1505 * Return an array of package options. 1506 * 1507 * @return Array 1508 */ 1509 protected function get_package_options() { 1510 1511 $packages = wp_cache_get( 'packages', $this->plugin_prefix ); 1512 if( ! empty( $packages ) ) { 1513 return $packages; 1514 } 1515 1516 $global_carriers= $this->shipStationApi->get_carriers(); 1517 $carrier_codes = wp_list_pluck( $global_carriers, 'carrier_code' ); 1518 $carrier_codes = array_intersect_key( $carrier_codes, array_flip( $this->carriers ) ); 1519 1520 $data = array( 1521 'usps' => array( 1522 'label' => esc_html__( 'USPS', 'live-rates-for-shipstation' ), 1523 'packages' => json_decode( file_get_contents( \IQLRSS\Driver::get_asset_path( 'json/usps-packages.json' ) ), true ), 1524 ), 1525 'ups' => array( 1526 'label' => esc_html__( 'UPS', 'live-rates-for-shipstation' ), 1527 'packages' => json_decode( file_get_contents( \IQLRSS\Driver::get_asset_path( 'json/ups-packages.json' ) ), true ), 1528 ), 1529 'fedex' => array( 1530 'label' => esc_html__( 'FedEx', 'live-rates-for-shipstation' ), 1531 'packages' => json_decode( file_get_contents( \IQLRSS\Driver::get_asset_path( 'json/fedex-packages.json' ) ), true ), 1532 ), 1533 ); 1534 1535 // Append Translated Labels 1536 $carrier_packages = array(); 1537 foreach( $data as $carrier_code => &$carriers ) { 1538 1539 // Match carrier slug with known carrier code. 1540 $carrier_found = array_filter( $carrier_codes, fn( $c ) => $c === $carrier_code ); 1541 if( empty( $carrier_found ) ) { 1542 $carrier_found = array_filter( $carrier_codes, fn( $c ) => false !== strpos( $c, $carrier_code . '_' ) ); 1543 } 1544 1545 // Skip - Carrier may not be set. 1546 if( empty( $carrier_found ) ) continue; 1547 1548 $codes = wp_list_pluck( $carriers['packages'], 'code' ); 1549 $dupes = array_count_values( $codes ); 1550 1551 foreach( $carriers['packages'] as &$package ) { 1552 1553 $package['carrier_code'] = $carrier_code; 1554 $package['label'] = $this->get_package_label( $package['code'] ); 1555 1556 if( $dupes[ $package['code'] ] > 1 ) { 1557 $package['label'] .= sprintf( ' (%s x %s x %s)', $package['length'], $package['width'], $package['height'] ); 1558 } 1559 } 1560 1561 usort( $carriers['packages'], fn( $pa, $pb ) => strcmp( $pa['label'], $pb['label'] ) ); 1562 $carrier_packages[ $carrier_code ] = $carriers; 1563 1564 } 1565 1566 $data = array( '' => esc_html__( '-- Select Package Preset --', 'live-rates-for-shipstation' ) ) + $carrier_packages; 1567 1568 1569 /** 1570 * Allow hooking into Custom Package presets for 3rd party management. 1571 * 1572 * @hook filter 1573 * 1574 * @param Array $data - Array( Array( 1575 * 'label' => 'Optional Optgroup Name', 1576 * 'packages' => Array( 1577 * 'label' => '', 1578 * 'code' => '', 1579 * 'length' => 0, 1580 * 'width' => 0, 1581 * 'height' => 0, 1582 * 'weight_max' => 0, 1583 * 'carrier_code' => '', 1584 * ) 1585 * ) ) 1586 * @param \IQLRSS\Core\Shipping_Method_Shipstation $this 1587 * 1588 * @return Array $data 1589 */ 1590 $packages = apply_filters( 'iqlrss/zone/package_presets', $data, $this ); 1591 1592 // Maybe reset if what we're given is not what we expect. 1593 if( ! is_array( $packages ) ) $packages = $data; 1594 1595 // Cache results to avoid multiple file reads per request. 1596 if( ! empty( $packages ) ) { 1597 wp_cache_add( 'packages', $packages, $this->plugin_prefix ); 1598 1599 // Maybe provide a default options / text when empty. 1600 } else { 1601 $packages = array( '' => esc_html__( 'No package presets.', 'live-rates-for-shipstation' ) ); 1602 } 1603 1604 return $packages; 1605 1606 } 1607 1608 1609 /** 1216 1610 * Format a stringified product name. 1217 1611 * ex. 213|Shirt|optional|meta|data … … 1223 1617 * @return String $name 1224 1618 */ 1225 p ublicfunction format_shipitem_name( $shipitem_name, $link = false, $context = 'edit' ) {1619 protected function format_shipitem_name( $shipitem_name, $link = false, $context = 'edit' ) { 1226 1620 1227 1621 $name = mb_strimwidth( $shipitem_name, 0, 47, '...' ); -
live-rates-for-shipstation/trunk/core/wc-box-packer/class-wc-boxpack-box.php
r3376459 r3407166 2 2 /** 3 3 * Box Packing class found in woocommerce-shipping-ups 4 * Updated by IQComputing because many of these methods 5 * have the wrong return documentation. 4 * Updated by IQComputing 6 5 * 7 6 * @version 2.0.1 … … 68 67 69 68 /** 70 * __construct function. 71 * 72 * @access public 69 * Box info - contains the core box properties and 70 * additonal data like nickname and price. 71 * See the Shipping Method Custom Packing Boxes for more info. 72 * 73 * @var Array 74 */ 75 private $data = array(); 76 77 78 /** 79 * Setup box properties. 80 * 81 * @param Array $box - Array( 'outer' => array( 'length', 'width', 'height' ), 'inner' => array( see outer ) ) 82 * 73 83 * @return void 74 84 */ 75 public function __construct( $length, $width, $height, $weight = 0 ) { 76 $dimensions = array( $length, $width, $height ); 77 78 sort( $dimensions ); 79 80 $this->outer_length = $this->length = floatval( $dimensions[2] ); 81 $this->outer_width = $this->width = floatval( $dimensions[1] ); 82 $this->outer_height = $this->height = floatval( $dimensions[0] ); 83 $this->weight = floatval( $weight ); 85 public function __construct( $box ) { 86 87 // Default - All Outer 88 $this->length = floatval( $box['outer']['length'] ); 89 $this->width = floatval( $box['outer']['width'] ); 90 $this->height = floatval( $box['outer']['height'] ); 91 $this->outer_length = floatval( $box['outer']['length'] ); 92 $this->outer_width = floatval( $box['outer']['width'] ); 93 $this->outer_height = floatval( $box['outer']['height'] ); 94 95 // Inner 96 if( ! empty( array_filter( (array)$box['inner'] ) ) ) { 97 $this->length = floatval( $box['inner']['length'] ); 98 $this->width = floatval( $box['inner']['width'] ); 99 $this->height = floatval( $box['inner']['height'] ); 100 } 101 102 // Weight 103 $this->weight = floatval( $box['weight'] ); 104 105 // Everything else 106 $this->data = $box; 107 84 108 } 85 109 … … 211 235 $this->reset_packed_dimensions(); 212 236 213 // @todo Rememer this kind of loop, neat method, love it.214 237 while ( sizeof( $items ) > 0 ) { 215 238 $item = array_shift( $items ); … … 268 291 $package->height = $this->get_outer_height(); 269 292 $package->value = $packed_value; 293 $package->data = $this->data; 270 294 271 295 // Calculate packing success % based on % of weight and volume of all items packed … … 281 305 } 282 306 307 // Fallback to amount packed 283 308 if ( is_null( $packed_weight_ratio ) && is_null( $packed_volume_ratio ) ) { 284 // Fallback to amount packed285 309 $package->percent = ( sizeof( $packed ) / ( sizeof( $unpacked ) + sizeof( $packed ) ) ) * 100; 310 311 // Volume only 286 312 } elseif ( is_null( $packed_weight_ratio ) ) { 287 // Volume only288 313 $package->percent = $packed_volume_ratio * 100; 314 315 // Weight only 289 316 } elseif ( is_null( $packed_volume_ratio ) ) { 290 // Weight only291 317 $package->percent = $packed_weight_ratio * 100; 318 319 // Default? 292 320 } else { 293 321 $package->percent = $packed_weight_ratio * $packed_volume_ratio * 100; … … 388 416 return $this->packed_length; 389 417 } 418 419 /** 420 * Return box data. 421 * 422 * @param String $key 423 * @param Mixed $default 424 * 425 * @return Mixed 426 */ 427 public function get_data( $key, $default = '' ) { 428 return ( isset( $this->data[ $key ] ) ) ? $this->data[ $key ] : $default; 429 } 390 430 } -
live-rates-for-shipstation/trunk/core/wc-box-packer/class-wc-boxpack-item.php
r3339099 r3407166 2 2 /** 3 3 * Box Packing class found in woocommerce-shipping-ups 4 * Updated by IQComputing because many of these methods 5 * have the wrong return documentation. 4 * Updated by IQComputing 6 5 * 7 6 * @version 2.0.1 -
live-rates-for-shipstation/trunk/core/wc-box-packer/class-wc-boxpack.php
r3376459 r3407166 2 2 /** 3 3 * Box Packing class found in woocommerce-shipping-ups 4 * Updated by IQComputing because many of these methods 5 * have the wrong return documentation. 4 * Updated by IQComputing 6 5 * 7 6 * @version 2.0.1 … … 70 69 * 71 70 * @access public 72 * @param mixed $length 73 * @param mixed $width 74 * @param mixed $height 75 * @param mixed $weight 71 * @param Array $box - Array( 'outer' => array( 'length', 'width', 'height' ), 'inner' => array( see outer ) ) 76 72 * @return object WC_Boxpack_Box 77 73 */ 78 public function add_box( $ length, $width, $height, $weight = 0) {79 $new_box = new WC_Boxpack_Box( $ length, $width, $height, $weight);74 public function add_box( $box ) { 75 $new_box = new WC_Boxpack_Box( $box ); 80 76 $this->boxes[] = $new_box; 81 77 return $new_box; … … 174 170 $package->unpacked = true; 175 171 $package->packed = array( $item ); 172 $package->data = array(); 176 173 $this->packages[] = $package; 177 174 } -
live-rates-for-shipstation/trunk/live-rates-for-shipstation.php
r3376459 r3407166 4 4 * Plugin URI: https://iqcomputing.com/contact/ 5 5 * Description: ShipStation shipping method with live rates. 6 * Version: 1. 0.87 * Requries at least: 5.96 * Version: 1.1.0 7 * Requries at least: 6.2 8 8 * Author: IQComputing 9 9 * Author URI: https://iqcomputing.com/ … … 12 12 * Text Domain: live-rates-for-shipstation 13 13 * Requires Plugins: woocommerce, woocommerce-shipstation-integration 14 *15 * @notes ShipStation does not make it easy or obvious how to update / create a Shipment for an Order.16 * The shipment create endpoint keeps coming back successful, but nothing on the ShipStation side17 * appears to change.18 * The v1 API update Order endpoint also doesn't seem to allow Shipment updates, but is required19 * to get the OrderID, required for any kind of create/update endpoints.20 *21 * @todo Add warehosue locations to Shipping Zone packages.22 * @todo Look into updating warehouses through Edit Order > Order Items.23 14 */ 24 15 namespace IQLRSS; … … 35 26 * @var String 36 27 */ 37 protected static $version = '1. 0.8';28 protected static $version = '1.1.0'; 38 29 39 30 … … 86 77 * @param Mixed $value 87 78 * 88 * @return Mixed79 * @return void 89 80 */ 90 81 public static function set_ss_opt( $key, $value ) { … … 105 96 106 97 /** 98 * Return a ShipStation Plugin Option Value 99 * 100 * @param String $key 101 * @param Mixed $default 102 * @param Boolean $skip_prefix - Skip Plugin Prefix and return a core ShipStation setting value. 103 * 104 * @return Mixed 105 */ 106 public static function get_opt( $key, $default = '' ) { 107 $settings = get_option( static::plugin_prefix( 'plugin' ) ); 108 return ( isset( $settings[ $key ] ) && '' !== $settings[ $key ] ) ? maybe_unserialize( $settings[ $key ] ) : $default; 109 } 110 111 112 /** 113 * Set a plugin option. 114 * 115 * @param String $key 116 * @param Mixed $value 117 * 118 * @return void 119 */ 120 public static function set_opt( $key, $value ) { 121 122 $option = static::plugin_prefix( 'plugin' ); 123 $settings = get_option( $option, array() ); 124 125 if( is_bool( $value ) ) { 126 $settings[ $key ] = boolval( $value ); 127 } else if( is_string( $value ) || is_numeric( $value ) ) { 128 $settings[ $key ] = sanitize_text_field( $value ); 129 } 130 131 update_option( $option, $settings ); 132 133 } 134 135 136 /** 137 * Clear the Plugin API cache. 138 * 139 * @return void 140 */ 141 public static function clear_cache() { 142 143 global $wpdb; 144 145 /** 146 * The API Class creates various transients to cache carrier services. 147 * These transients are not tracked but generated based on the responses carrier codes. 148 * All these transients are prefixed with our plugins unique string slug. 149 * The first WHERE ensures only `_transient_` and the 2nd ensures only our plugins transients. 150 */ 151 $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE option_name LIKE %s AND option_name LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnsupportedIdentifierPlaceholder 152 $wpdb->options, 153 $wpdb->esc_like( '_transient_' ) . '%', 154 '%' . $wpdb->esc_like( '_' . static::get( 'slug' ) . '_' ) . '%' 155 ) ); 156 157 // Set transient to clear any WC_Session caches if they are found. 158 $expires = absint( apply_filters( 'wc_session_expiration', DAY_IN_SECONDS * 2 ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 159 set_transient( static::plugin_prefix( 'wcs_timeout' ), time(), $expires ); 160 161 } 162 163 164 /** 107 165 * Prefix a string with the plugin slug. 108 166 * … … 124 182 125 183 /** 126 * Return a URL to an asset (JS/CSS )184 * Return a URL to an asset (JS/CSS usually) 127 185 * 128 186 * @param String $asset 129 187 * 130 * @return String $url188 * @return String 131 189 */ 132 190 public static function get_asset_url( $asset ) { … … 141 199 142 200 /** 201 * Return a path to an asset. 202 * 203 * @param String $asset 204 * 205 * @return String 206 */ 207 public static function get_asset_path( $asset ) { 208 209 return sprintf( '%s/core/assets/%s', 210 rtrim( plugin_dir_path( __FILE__ ), '\\/' ), 211 $asset 212 ); 213 214 } 215 216 217 /** 143 218 * Initialize the core controllers 144 219 * Vroom! … … 147 222 */ 148 223 public static function drive() { 224 225 // Run any version transition actions. 226 Stallation::transversion( static::$version ); 227 228 // Load core controllers. 229 Core\Rest_Router::initialize(); 149 230 Core\Settings_Shipstation::initialize(); 150 231 } -
live-rates-for-shipstation/trunk/readme.txt
r3376459 r3407166 4 4 Requires at least: 5.9 5 5 Tested up to: 6.8 6 Stable tag: 1. 0.86 Stable tag: 1.1.0 7 7 License: GPLv3 or later 8 8 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 16 16 This plugin connects to the ShipStation API using an authentication key to display shipping rates from various common carriers supported by ShipStation. This allows store owners to group all their shipping carriers under one umbrella which makes management easier and allows customers to choose the best shipping method for them which leads to happier customers. 17 17 18 In order to use the Live Rates for ShipStation plugin, you must have a [premium ShipStation account](https://www. dpbolvw.net/click-101532691-11646582), and purchased the [ShipStation for WooCommerce](https://woocommerce.com/products/shipstation-integration/) plugin. This plugin **will not work** without access to the ShipStation API which is tied to your premium ShipStation account.18 In order to use the Live Rates for ShipStation plugin, you must have a [premium ShipStation account](https://www.kqzyfj.com/click-101532691-15733876), and purchased the [ShipStation for WooCommerce](https://woocommerce.com/products/shipstation-integration/) plugin. This plugin **will not work** without access to the ShipStation API which is tied to your premium ShipStation account. 19 19 20 20 Please review [ShipStations Terms of Service](https://www.shipstation.com/terms-of-service/) and [ShipStations Privacy Policy](https://auctane.com/legal/privacy-policy/) for more information about how your data is managed. 21 21 22 Don't have a ShipStation account? [Open a ShipStation account today!](https://www. dpbolvw.net/click-101532691-11646582)22 Don't have a ShipStation account? [Open a ShipStation account today!](https://www.kqzyfj.com/click-101532691-15733876) 23 23 24 24 == Plugin Requirements == 25 25 26 1. [A Premium ShipStation Account](https://www. dpbolvw.net/click-101532691-11646582)26 1. [A Premium ShipStation Account](https://www.kqzyfj.com/click-101532691-15733876) 27 27 1. [The WooCommerce Plugin](https://wordpress.org/plugins/woocommerce/) 28 28 1. [The ShipStation for WooCommerce Plugin](https://woocommerce.com/products/shipstation-integration/) … … 51 51 == Changelog == 52 52 53 = 1.1.0 (2025-12-01) = 54 * Redux the Custom Packaging screen and options. 55 * Packing option for Weight Only. 56 * Packing option for Stacked Vertically. 57 * Packing option for default product weight. 58 * Custom Package Presets from UPS, FedEx, and USPS. 59 * New filter hook for Shipping Zone Settings `iqlrss/zone/settings`. Useful for managing Product Packing options. 60 * New filter hook for Shipping Zone Settings `iqlrss/zone/package_presets`. Useful for managing Custom Package presets. 61 * New filter hook for Shipping Estimates `iqlrss/shipping/packages`. Useful for modifying what gets sent to ShipStation API for retrieving shipping estimates. 62 53 63 = 1.0.8 (2025-10-10) = 54 64 * Patches issue of missing `other_amount` when applying shipping rates (thanks @centuryperf)!
Note: See TracChangeset
for help on using the changeset viewer.