Created
April 13, 2015 19:00
-
-
Save grundyoso/2975da69fee97f4a95b6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require_once('easypost-php/lib/easypost.php'); | |
add_action('woocommerce_product_options_shipping', 'add_pricing_input'); | |
add_action('save_post', 'set_customs_value'); | |
/** | |
* Add a customs value field in the WooCommerce Product form (for Admins) | |
* @see add_pricing_input() | |
*/ | |
function add_pricing_input($content) | |
{ | |
global $post; | |
woocommerce_wp_text_input(array( | |
'id' => '_customs_value', | |
'class' => 'short', | |
'name' => 'wc_customs_value', | |
'type' => 'number', | |
'label' => __( 'Customs Value', 'woocommerce' ), | |
) | |
); | |
} | |
/** | |
* If exists, add customs value to the $_POST data of the product | |
* @see set_customs_value() | |
*/ | |
function set_customs_value($post) | |
{ | |
if( array_key_exists('wc_customs_value',$_POST) ){ | |
update_post_meta($_POST['post_ID'], '_customs_value',$_POST['wc_customs_value']); | |
} | |
} | |
class WC_EasyPost extends WC_Shipping_Method { | |
/** | |
* Construct the EasyPost shipping method, initializing key values | |
*/ | |
function __construct() | |
{ | |
$this->id = 'easypost'; | |
$this->has_fields = true; | |
$this->init_form_fields(); | |
$this->init_settings(); | |
$this->title = __('EasyPost', 'woocommerce'); | |
$this->usesandboxapi = strcmp($this->settings['test'], 'yes') == 0; | |
$this->testApiKey = $this->settings['test_api_key' ]; | |
$this->liveApiKey = $this->settings['live_api_key' ]; | |
$this->handling = $this->settings['handling'] ? $this->settings['handling'] : 0; | |
$this->filters = explode(",", $this->settings['filter_rates']); | |
$this->secret_key = $this->usesandboxapi ? $this->testApiKey : $this->liveApiKey; | |
\EasyPost\EasyPost::setApiKey($this->secret_key); | |
$this->enabled = $this->settings['enabled']; | |
// Check if a previous instance of WC_EasyPost has subscribed to the woocommerce_update_options_shipping_easypost action | |
// There should only be one subscription, otherwise there will be duplicate notifications rendered on form errors. | |
if ( !has_action( 'woocommerce_update_options_shipping_' . $this->id) ) { | |
add_action('woocommerce_update_options_shipping_' . $this->id , array($this, 'process_admin_options')); | |
} | |
// Add this action registration to catch the case when customers create an account and checkout without | |
// explicitly adding a shipping address. | |
//add_action('woocommerce_review_order_before_submit', array(&$this, 'calculate_shipping' )); | |
// Uncomment following line if you want customer pruchases to automatically purchase shipping labels (same day fulfillment) | |
//add_action('woocommerce_checkout_order_processed', array(&$this, 'purchase_order' )); | |
} | |
/** | |
* Display form fields by defining init_form_fields() method | |
* @see init_form_fields() | |
*/ | |
public function init_form_fields() | |
{ | |
$this->form_fields = array( | |
'enabled' => array( | |
'title' => __( 'Enable/Disable', 'woocommerce' ), | |
'type' => 'checkbox', | |
'label' => __( 'Enabled', 'woocommerce' ), | |
'default' => 'yes', | |
), | |
'filter_rates' => array( | |
'title' => __( 'Filter these rates', 'woocommerce' ), | |
'desc_tip' => __( 'Strings included will be left out of checkout options.', 'woocommerce' ), | |
'type' => 'text', | |
'label' => __( 'Fitler (Comma Seperated)', 'woocommerce' ), | |
'default' => ('LibraryMail'), | |
), | |
'test' => array( | |
'title' => __( 'Test Mode', 'woocommerce' ), | |
'type' => 'checkbox', | |
'label' => __( 'Enabled', 'woocommerce' ), | |
'default' => 'yes', | |
), | |
'test_api_key' => array( | |
'title' => "Test Api Key", | |
'desc_tip' => __( 'Get this from your EasyPost account dashboard.', 'woocommerce' ), | |
'type' => 'text', | |
'label' => __( 'Test Api Key', 'woocommerce' ), | |
'default' => '', | |
), | |
'live_api_key' => array( | |
'title' => "Live Api Key", | |
'desc_tip' => __( 'Get this from your EasyPost account dashboard.', 'woocommerce' ), | |
'type' => 'text', | |
'label' => __( 'Live Api Key', 'woocommerce' ), | |
'default' => '', | |
), | |
'handling' => array( | |
'title' => "Handling Charge", | |
'desc_tip' => __( 'This amount is added to the shipping charge.', 'woocommerce' ), | |
'type' => 'number', | |
'label' => __( 'Handling Charge', 'woocommerce' ), | |
'default' => '0', | |
), | |
'company' => array( | |
'title' => "Company", | |
'type' => 'text', | |
'label' => __( 'Company', 'woocommerce' ), | |
'default' => '', | |
), | |
'street1' => array( | |
'title' => 'Address', | |
'type' => 'text', | |
'label' => __( 'Address', 'woocommerce' ), | |
'default' => '', | |
), | |
'street2' => array( | |
'title' => 'Address2', | |
'type' => 'text', | |
'label' => __( 'Address2', 'woocommerce' ), | |
'default' => '', | |
), | |
'city' => array( | |
'title' => 'City', | |
'type' => 'text', | |
'label' => __( 'City', 'woocommerce' ), | |
'default' => '', | |
), | |
'state' => array( | |
'title' => 'State', | |
'type' => 'text', | |
'label' => __( 'State', 'woocommerce' ), | |
'default' => '', | |
), | |
'zip' => array( | |
'title' => 'Zip', | |
'type' => 'number', | |
'label' => __( 'ZipCode', 'woocommerce' ), | |
'default' => '', | |
), | |
'country' => array( | |
'title' => 'Country', | |
'type' => 'text', | |
'label' => __( 'Country', 'woocommerce' ), | |
'default' => 'US', | |
), | |
'contact' => array( | |
'title' => 'Contact', | |
'desc_tip' => __( 'A contact name is needed for international customs forms.', 'woocommerce' ), | |
'type' => 'text', | |
'label' => __( 'Contact (Customs Forms)', 'woocommerce' ), | |
'default' => '', | |
), | |
'phone' => array( | |
'title' => 'Phone', | |
'desc_tip' => __( 'Follow the format XXXXXXXXXX.', 'woocommerce' ), | |
'type' => 'number', | |
'label' => __( 'Phone', 'woocommerce' ), | |
'default' => '', | |
), | |
'hs_tariff_number' => array( | |
'title' => 'HS Tariff Number', | |
'desc_tip' => __( 'You can search for HS tariff codes online.', 'woocommerce' ), | |
'type' => 'number', | |
'label' => __( 'HS Tariff Number', 'woocommerce' ), | |
'default' => '851762', | |
), | |
); | |
} | |
/** | |
* Validate the live_api_key field | |
* @see validate_settings_fields() | |
*/ | |
public function validate_live_api_key_field( $key ) { | |
// get the posted value | |
$value = $_POST[ $this->plugin_id . $this->id . '_' . $key ]; | |
// check if the API key is not 23 characters. Throw an error which will prevent the user from saving. | |
if ( strlen( $value ) != 22 ) { | |
$this->errors[] = $key; | |
} | |
return $value; //return the sanitized $value (if any) | |
} | |
/** | |
* Validate the phone field | |
* @see validate_settings_fields() | |
*/ | |
public function validate_phone_field( $key ) { | |
// get the posted value | |
$value = $_POST[ $this->plugin_id . $this->id . '_' . $key ]; | |
// check if phone matches the XXXXXXXXXX format | |
if ( !preg_match('/\d{10}/',$value) || strlen( $value ) != 10) { | |
$this->errors[] = $key; | |
} | |
return $value; //return the sanitized $value (if any) | |
} | |
/** | |
* Display errors by overriding the display_errors() method | |
* @see display_errors() | |
*/ | |
public function display_errors( ) { | |
// loop through each error and display it | |
foreach ( $this->errors as $key => $value ) { | |
?> | |
<div class="error"> | |
<p><?php _e( 'Looks like you made a mistake with the <b>' . $value . '</b> field.', 'woocommerce' ); ?></p> | |
</div> | |
<?php | |
} | |
} | |
/** | |
* This is an overloaded function called by the Woocommerce Cart process | |
* @see calculate_shipping() | |
*/ | |
function calculate_shipping($packages = array()) | |
{ | |
$shipment = $this->create_shipment($packages); | |
$created_rates = \EasyPost\Rate::create($shipment); | |
$this->gather_rates($shipment, $created_rates); | |
} | |
/** | |
* This is an overloaded function called by the Woocommerce Cart process | |
* @see calculate_shipping() | |
*/ | |
function calculate_shipping_admin_dropdown($order_id) | |
{ | |
$output = ''; | |
$order = new WC_Order($order_id); | |
$shipment = $this->create_shipment($order); | |
$created_rates = \EasyPost\Rate::create($shipment); | |
$method = array_values($order->get_shipping_methods()); | |
if (empty($method)) { | |
return '<p>No shipping methods chosen. Seems the order is purely virtual.</p>'; | |
} | |
//echo "<pre>". print_r($method)."</pre>"; | |
$shipping_method_arr = explode('|',$method[0]['name']); | |
$this->gather_rates($shipment, $created_rates); | |
$output .= "<p>Customer Paid for: ".$method[0]['name'] ." $".$method[0]['cost']."</p>"; | |
$output .= "<form action='#' method='post' name='shipping_rate' id='shipping_rate'>"; | |
if(empty($this->rates)){ | |
$output .="<p>No Rates Found.</p>"; | |
}else{ | |
$output .="<select name='shipping_rate_meta_box' id='shipping_rate_meta_box'>"; | |
foreach($this->rates as $idx => $rate) // go through the gathered rates and display in dropdown | |
{ | |
$selected = ($method[0]['name'] == $rate->label ? 'selected':''); | |
$output .="<option value='".$rate->id."' ".$selected.">".$rate->label ." $". $rate->cost ."</option>"; | |
} | |
$output .="</select>"; | |
} | |
$output .="<input type='submit' value='Create Shipping Label' class='button button-primary tips' />"; | |
$output .="</form>"; | |
return $output; | |
} | |
/** | |
* This function can be called via the WooCommerce Cart or from the Admin Order Dashboard. We need to account for the | |
* two cases separately since the data sources are different. After collecting the relevant data, we create a shipment with | |
* EasyPost and get the rate listing. | |
*/ | |
function create_shipment($order = array()) | |
{ | |
global $woocommerce, $GLOBALS; | |
$wp_admin = new WP_User($GLOBALS['current_user']->ID); // logged in admin performing shipping function | |
try | |
{ | |
if (!empty($woocommerce->customer)) { // if customer is defined then we are in the WooCommerce Cart process | |
$line_items = $woocommerce->cart->get_cart(); | |
$quantity_string = 'quantity'; | |
$customer_name = ''; // not available in Cart process | |
$customer_company = ''; // not available in Cart process | |
$customer_phone = ''; // not available in Cart process | |
$customer_email = ''; // not available in Cart process | |
$customer_street1 = $woocommerce->customer->get_address(); | |
$customer_street2 = $woocommerce->customer->get_address_2(); | |
$customer_city = $woocommerce->customer->get_city(); | |
$customer_state = $woocommerce->customer->get_state(); | |
$customer_zip = $woocommerce->customer->get_postcode(); | |
$customer_country = $woocommerce->customer->get_country(); | |
} else if (!empty($order)) { // if $order is passed in then we are in the Admin Order Dashboard | |
$line_items = $order->get_items(); | |
$quantity_string = 'qty'; | |
$customer_name = sprintf("%s %s", $order->shipping_first_name, $order->shipping_last_name); | |
$customer_company = $order->shipping_company; | |
$customer_phone = $order->billing_phone; | |
$customer_email = $order->billing_email; | |
$customer_street1 = $order->shipping_address_1; | |
$customer_street2 = $order->shipping_address_2; | |
$customer_city = $order->shipping_city; | |
$customer_state = $order->shipping_state; | |
$customer_zip = $order->shipping_postcode; | |
$customer_country = $order->shipping_country; | |
} else { | |
throw new Exception('No customer or order id detected. Check the source.'); | |
} | |
foreach($line_items as $line_item) // go through all the line_items to calculate package size and weight | |
{ | |
$product = get_product($line_item['product_id']); | |
$dimensions = $product->get_dimensions(); | |
$dimensions_arr = explode('x', trim(str_replace('in','',$dimensions))); // parse into array removing woocommerce 'in' units | |
if (!empty($dimensions)) { // if no dimensions it's a virtual product and won't be shipped, so disregard. | |
$length[] = floatval($dimensions_arr[0]); | |
$width[] = floatval($dimensions_arr[1]); | |
$height[] = $dimensions_arr[2] * $line_item[$quantity_string]; // make sure cart or order qantitiy is accounted for | |
$weight[] = $product->get_weight() * 16 * $line_item[$quantity_string]; // convert woocommerce lbs to ozs | |
$customs_value = get_post_meta($product->id, '_customs_value'); | |
$customs_value_check = !empty( $customs_value ) ? $customs_value[0] * $line_item[$quantity_string] : 0; | |
$customs_item[] = \EasyPost\CustomsItem::create( | |
array( | |
"description" => $product->get_title(), | |
"quantity" => $line_item[$quantity_string], | |
"value" => $customs_value_check, | |
"weight" => $product->get_weight() * 16 * $line_item[$quantity_string], // convert woocommerce lbs to ozs | |
"hs_tariff_number" => $this->settings['hs_tariff_number'], // from Harmonized System Codes http://hts.usitc.gov/ | |
"origin_country" => $this->settings['country'], // assume the configured company address is manufacturer | |
) | |
); | |
} | |
} | |
// get the max length and width | |
$net_length = is_array($length) ? max($length) : $length; | |
$net_width = is_array($width) ? max($width) : $width; | |
// get the aggregate height and weight | |
$net_height = is_array($height) ? array_sum($height) : $height; | |
$net_weight = is_array($weight) ? array_sum($weight)+1 : $weight; // TODO: This "+1" is a hack to address isuue #126, need to report to Easypost because this was working before and now the API rejects it without the hack. | |
// Create the destination address object for customer, enter all available entries | |
$to_address = \EasyPost\Address::create( | |
array( | |
"name" => $customer_name, | |
"company" => $customer_company, | |
"phone" => $customer_phone, | |
"email" => $customer_email, | |
"street1" => $customer_street1, | |
"street2" => $customer_street2, | |
"city" => $customer_city, | |
"state" => $customer_state, | |
"zip" => $customer_zip, | |
"country" => $customer_country, | |
) | |
); | |
// Create the source address object for company, enter all available entries from init_form_fields() settings above | |
$from_address = \EasyPost\Address::create( | |
array( | |
"company" => $this->settings['company'], | |
"phone" => $this->settings['phone'], | |
"street1" => $this->settings['street1'], | |
"street2" => $this->settings['street2'], | |
"city" => $this->settings['city'], | |
"state" => $this->settings['state'], | |
"zip" => $this->settings['zip'], | |
"country" => $this->settings['country'], | |
) | |
); | |
// Create a parcel object based on the package size and weight. We default to USPS 'FlatRatePaddedEnvelope' | |
// Review https://www.easypost.com/docs/api/php#predefined-packages for other packages to choose as default. | |
// Just make sure to update ($net_length < 12) && ($net_width < 9) && ($net_weight < 4*16) based on package package | |
// Note: Weight in EasyPost is OZs and Woocommerce stores LBs so need to mulitply by 16 to convert | |
$parcel = \EasyPost\Parcel::create( array( | |
"length" => $net_length, | |
"width" => $net_width, | |
"height" => $net_height, | |
"predefined_package" => null, | |
// "predefined_package" => 'FlatRatePaddedEnvelope', | |
"weight" => $net_weight | |
) | |
); | |
// Create the customs info object with mostly hardcoded values and the $customs_item array | |
$customs_info = \EasyPost\CustomsInfo::create( | |
array( | |
"eel_pfc" => 'NOEEI 30.37(a)', // if under $2500 in value, this setting works. | |
"contents_type" => 'merchandise', // should work for most web stores | |
"contents_explanation" => '', | |
"customs_certify" => true, | |
"customs_signer" => $this->settings['contact'], // set admin shipping parcels | |
"non_delivery_option" => 'return', | |
"restriction_type" => 'none', | |
'restriction_comments' => '', | |
'customs_items' => $customs_item, // array of customs items assembled above | |
) | |
); | |
// Create a shipment object with the settings above. Parse some filter names to map to their | |
// respective EasyPost defined strings. This shipment will be available from EasyPost for later | |
// reference. | |
if(empty($this->filters)){ | |
$ship_options = array(); | |
} elseif (!in_array('MediaMail', $this->filters)) { | |
$ship_options = array('special_rates_eligibility' => 'USPS.MEDIAMAIL'); | |
} elseif (!in_array('LibraryMail', $this->filters)) { | |
$ship_options = array('special_rates_eligibility' => 'USPS.LIBRARYMAIL'); | |
} else { | |
$ship_options = array(); | |
} | |
$shipment = \EasyPost\Shipment::create( | |
array( | |
"to_address" => $to_address, | |
"from_address" => $from_address, | |
"parcel" => $parcel, | |
// "customs_info" => $customs_info, | |
"options" => $ship_options, | |
"carrier_accounts" => 'ca_DDQVzUhQ' | |
) | |
); | |
return $shipment; | |
} | |
catch(Exception $e) | |
{ | |
// EasyPost Error - Lets Log. | |
error_log(var_export($e,1)); | |
return false; | |
} | |
} | |
/** | |
* This function gathers the rates of a EasyPost shipment entry. Filters out rates list in the settings form | |
*/ | |
function gather_rates($shipment, $created_rates) | |
{ | |
foreach($created_rates as $r) | |
{ | |
$rate = array( | |
'id' => sprintf("easypost|%s-%s|%s", $r->carrier, $r->service, $shipment->id), //'easypost' must be prefix for default | |
'label' => sprintf("%s %s", $r->carrier , $r->service), | |
'cost' => $r->rate + $this->handling, | |
'calc_tax' => 'per_item' | |
); | |
error_log('$$rate ::::> ' . var_export($rate,1)); | |
$filter_out = !empty($this->filters) ? $this->filters : array('LibraryMail'); | |
if (!in_array($r->service, $filter_out)) | |
{ | |
// Register the rate | |
$this->add_rate( $rate ); | |
} | |
} | |
} | |
/** | |
* This is an overloaded function called by the Woocommerce Cart or Admin process | |
* @see purchase_order() | |
*/ | |
public function purchase_order($order_id) | |
{ | |
global $wpdb; | |
try | |
{ | |
$order = new WC_Order($order_id); | |
$method = array_values($order->get_shipping_methods()); | |
if (empty($method)) { | |
throw new Exception('No shipping methods chosen. Seems the order is purely virtual.'); | |
} | |
if(!isset($_REQUEST['shipping_rate_meta_box'])){ | |
throw new Exception('No shipping rate selected.'); | |
} | |
$shipping_method_arr = explode('|',$method[0]['method_id']); | |
if(count($shipping_method_arr) >= 3) // retrieve the shipment that was created by Woocommerce Cart | |
{ | |
try {$shipment = \EasyPost\Shipment::retrieve(array('id' => $shipping_method_arr[2]));} | |
catch(Exception $e) { | |
update_post_meta( $order_id, '_easypost_error', $e->getMessage() ); // post to meta so modal window can retireve it | |
error_log('easypost_error L486 ::::> ' . var_export($e,1)); // graceful log EasyPost error | |
// Despite exception caught here, we don't "return false;" but try and create a new shipment | |
// This is because switching betweem Test and Live API can leave transient shipments in EasyPost. These we can ignore. | |
} | |
} | |
if (!empty($shipment)) { | |
$new_shipment = false; // found a shipment, may not need to create new one | |
// Admin might have changed to_address by editting "Shipping Address", so check for that case. | |
// Woocommerce Cart leaves to_address partially empty, so fill missing fields now. | |
$to_address = $shipment->__get('to_address'); // Get the to_address instance from the shipment record. | |
if ( ($to_address['street1'] != $order->shipping_address_1) || | |
($to_address['street2'] != $order->shipping_address_2) || | |
($to_address['city'] != $order->shipping_city) || | |
($to_address['state'] != $order->shipping_state) || | |
($to_address['zip'] != $order->shipping_postcode) || | |
((empty($to_address['name'])) && (empty($to_address['company']))) ) // check to see if missing fields | |
{ | |
$new_shipment = true; // shipment found is bad, create a new one | |
} | |
$created_rates = $shipment->rates; | |
} | |
else | |
$new_shipment = true; // no shipment found, Admin might have shifted EasyPost accounts, create new shipment | |
if($new_shipment) { | |
// Create a new shipment with info from Admin Order Dashboard | |
// We need to persist the shipments so redundant labels aren't purchased by mistake | |
$order_item_id = $wpdb->get_results("SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = {$order_id} AND order_item_type = 'shipping'"); // query the woocommerce database for the shipping method chosen on checkout | |
//Update Method ID from selected rate | |
$selected_shipping_method = $_REQUEST['shipping_rate_meta_box']; | |
wc_update_order_item_meta( $order_item_id[0]->order_item_id, 'method_id', $selected_shipping_method ); | |
} | |
foreach($created_rates as $idx => $rate) // go through the available EasyPost options and match the chosen/persisted one | |
{ | |
if (!empty($shipping_method_arr[1]) && preg_match(sprintf("/%s-%s/", $rate->carrier, $rate->service), $shipping_method_arr[1])) | |
{ | |
$index = $idx; | |
break; | |
} | |
} | |
if (empty($shipment->rates[$index])) throw new Exception('Couldn\'t find a rate. Check line 272 or 360 in easypost_shipping.php.'); | |
if (empty($shipment->postage_label)) $shipment->buy($shipment->rates[$index]); // buy the label only if haven't been bought before | |
update_post_meta( $order_id, 'easypost_tracking_link', | |
'https://tools.usps.com/go/TrackConfirmAction!input.action?tRef=qt&tLc=1&tLabels=' . $shipment->tracking_code); | |
update_post_meta( $order_id, '_tracking_number', $shipment->tracking_code); // the preceding "_" hides meta from Admin Dash | |
update_post_meta( $order_id, '_shipping_label_size', $shipment->postage_label->label_size); // the preceding "_" hides meta | |
return $shipment->postage_label->label_url; | |
} | |
catch(Exception $e) | |
{ | |
update_post_meta( $order_id, '_easypost_error', $e->getMessage() ); // post to meta so modal window can retireve it | |
error_log('easypost_error L513 ::::> ' . var_export($e,1)); // graceful log EasyPost error | |
return false; | |
} | |
} | |
} | |
/** | |
* Add EasyPost shipping method to the WooCommerce Admin dashboard | |
* @see add_shipping_method() | |
*/ | |
function add_easypost_method( $methods ) { | |
$methods[] = 'WC_EasyPost'; return $methods; | |
} | |
add_filter('woocommerce_shipping_methods', 'add_easypost_method' ); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment