Skip to content

Instantly share code, notes, and snippets.

@grundyoso
Created April 13, 2015 19:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grundyoso/2975da69fee97f4a95b6 to your computer and use it in GitHub Desktop.
Save grundyoso/2975da69fee97f4a95b6 to your computer and use it in GitHub Desktop.
<?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