From 5590fc75a42012c467f0e3c36bd4bdb80c4de76a Mon Sep 17 00:00:00 2001 From: Jamie Isaacs Date: Thu, 25 Sep 2014 23:54:31 -0700 Subject: [PATCH] Adding initial shipping API version. --- README.md | 228 +++++++++++++++++- phpunit.xml | 17 ++ src/Arr.php | 10 + src/Fedex/Rate.php | 236 +++++++++++++++++++ src/RateAdapter.php | 64 +++++ src/RateRequest/Adapter.php | 10 + src/RateRequest/Get.php | 22 ++ src/RateRequest/Post.php | 23 ++ src/RateRequest/StubFedex.php | 23 ++ src/RateRequest/StubUPS.php | 24 ++ src/RateRequest/StubUSPS.php | 24 ++ src/Ship.php | 143 ++++++++++++ src/UPS/Rate.php | 247 ++++++++++++++++++++ src/USPS/Rate.php | 182 +++++++++++++++ tests/ShipTest.php | 425 ++++++++++++++++++++++++++++++++++ 15 files changed, 1676 insertions(+), 2 deletions(-) create mode 100644 phpunit.xml create mode 100644 src/Arr.php create mode 100644 src/Fedex/Rate.php create mode 100644 src/RateAdapter.php create mode 100644 src/RateRequest/Adapter.php create mode 100644 src/RateRequest/Get.php create mode 100644 src/RateRequest/Post.php create mode 100644 src/RateRequest/StubFedex.php create mode 100644 src/RateRequest/StubUPS.php create mode 100644 src/RateRequest/StubUSPS.php create mode 100644 src/Ship.php create mode 100644 src/UPS/Rate.php create mode 100644 src/USPS/Rate.php create mode 100644 tests/ShipTest.php diff --git a/README.md b/README.md index e5a3a5b..dff8d39 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,226 @@ -shipping -======== +PHP Shipping API +================ + +A shipping rate wrapper for USPS, UPS, and Fedex. + +## Installation + +Add the following lines to your ``composer.json`` file. + +```JSON +{ + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/pdt256/shipping.git" + } + ], + "require": { + "pdt256/shipping": "dev-master" + } +} +``` + +## Example + +Create a shipment object: + +```php +$shipment = [ + 'weight' => 3, // lbs + 'dimensions' => [ + 'width' => 9, // inches + 'length' => 9, + 'height' => 9, + ], + 'from' => [ + 'postal_code' => '90401', + 'country_code' => 'US', + ], + 'to' => [ + 'postal_code' => '78703', + 'country_code' => 'US', + 'is_residential' => TRUE, + ], +]; +``` + +## UPS (Stub) Example + +Below is an example request to get shipping rates from the UPS API. + +Notice: The below line uses a stub class to fake a response from the UPS API. +You can immediately use this method in your code until you get an account with UPS. +``` +'request_adapter' => new RateRequest\StubUPS(), +``` + +```php +use pdt256\Shipping\UPS; +use pdt256\Shipping\RateRequest; + +$ups = new UPS\Rate([ + 'prod' => FALSE, + 'access_key' => 'XXXX', + 'user_id' => 'XXXX', + 'password' => 'XXXX', + 'shipper_number' => 'XXXX', + 'shipment' => $shipment, + 'approved_codes' => [ + '03', // 1-5 business days + '02', // 2 business days + '01', // next business day 10:30am + '13', // next business day by 3pm + '14', // next business day by 8am + ], + 'request_adapter' => new RateRequest\StubUPS(), +]); + +$ups_rates = $ups->get_rates(); +``` + +Output array sorted by cost: (in cents) + +```php +array ( + 0 => + array ( + 'code' => '03', + 'name' => 'UPS Ground', + 'cost' => 1900, + ), + 1 => + array ( + 'code' => '02', + 'name' => 'UPS 2nd Day Air', + 'cost' => 4900, + ), + 2 => + array ( + 'code' => '13', + 'name' => 'UPS Next Day Air Saver', + 'cost' => 8900, + ), + 3 => + array ( + 'code' => '01', + 'name' => 'UPS Next Day Air', + 'cost' => 9300, + ), +) +``` + +## USPS (Stub) Example + +```php +use pdt256\Shipping\USPS; +use pdt256\Shipping\RateRequest; + +$usps = new USPS\Rate([ + 'prod' => FALSE, + 'username' => 'XXXX', + 'password' => 'XXXX', + 'shipment' => array_merge($shipment, [ + 'size' => 'LARGE', + 'container' => 'RECTANGULAR', + ]), + 'approved_codes' => [ + '1', // 1-3 business days + '4', // 2-8 business days + ], + 'request_adapter' => new RateRequest\StubUSPS(), +]); + +$usps_rates = $usps->get_rates(); +``` + +Output array sorted by cost: (in cents) + +```php +array ( + 1 => + array ( + 'code' => '4', + 'name' => 'Parcel Post', + 'cost' => 1000, + ), + 0 => + array ( + 'code' => '1', + 'name' => 'Priority Mail', + 'cost' => 1200, + ), +) +``` + +## Fedex (Stub) Example + +```php +use pdt256\Shipping\Fedex; +use pdt256\Shipping\RateRequest; + +$fedex = new Fedex\Rate([ + 'prod' => FALSE, + 'key' => 'XXXX', + 'password' => 'XXXX', + 'account_number' => 'XXXX', + 'meter_number' => 'XXXX', + 'drop_off_type' => 'BUSINESS_SERVICE_CENTER', + 'shipment' => array_merge($shipment, [ + 'packaging_type' => 'YOUR_PACKAGING', + ]), + 'approved_codes' => [ + 'FEDEX_EXPRESS_SAVER', // 1-3 business days + 'FEDEX_GROUND', // 1-5 business days + 'GROUND_HOME_DELIVERY', // 1-5 business days + 'FEDEX_2_DAY', // 2 business days + 'STANDARD_OVERNIGHT', // overnight + ], + 'request_adapter' => new RateRequest\StubFedex(), +]); + +$fedex_rates = $fedex->get_rates(); +``` + +Output array sorted by cost: (in cents) + +```php +array ( + 3 => + array ( + 'code' => 'GROUND_HOME_DELIVERY', + 'name' => 'Ground Home Delivery', + 'cost' => 1600, + 'delivery_ts' => NULL, + 'transit_time' => 'THREE_DAYS', + ), + 2 => + array ( + 'code' => 'FEDEX_EXPRESS_SAVER', + 'name' => 'Fedex Express Saver', + 'cost' => 2900, + 'delivery_ts' => '2014-09-30T20:00:00', + 'transit_time' => NULL, + ), + 1 => + array ( + 'code' => 'FEDEX_2_DAY', + 'name' => 'Fedex 2 Day', + 'cost' => 4000, + 'delivery_ts' => '2014-09-29T20:00:00', + 'transit_time' => NULL, + ), + 0 => + array ( + 'code' => 'STANDARD_OVERNIGHT', + 'name' => 'Standard Overnight', + 'cost' => 7800, + 'delivery_ts' => '2014-09-26T20:00:00', + 'transit_time' => NULL, + ), +) +``` + +### License + +[MIT license](http://opensource.org/licenses/MIT) diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e23b94a --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + ./tests + + + diff --git a/src/Arr.php b/src/Arr.php new file mode 100644 index 0000000..aba9ec4 --- /dev/null +++ b/src/Arr.php @@ -0,0 +1,10 @@ + 'Europe First International Priority', + 'FEDEX_1_DAY_FREIGHT' => 'Fedex 1 Day Freight', + 'FEDEX_2_DAY' => 'Fedex 2 Day', + 'FEDEX_2_DAY_AM' => 'Fedex 2 Day AM', + 'FEDEX_2_DAY_FREIGHT' => 'Fedex 2 Day Freight', + 'FEDEX_3_DAY_FREIGHT' => 'Fedex 3 Day Freight', + 'FEDEX_EXPRESS_SAVER' => 'Fedex Express Saver', + 'FEDEX_FIRST_FREIGHT' => 'Fedex First Freight', + 'FEDEX_FREIGHT_ECONOMY' => 'Fedex Freight Economy', + 'FEDEX_FREIGHT_PRIORITY' => 'Fedex Freight Priority', + 'FEDEX_GROUND' => 'Fedex Ground', + 'FIRST_OVERNIGHT' => 'First Overnight', + 'GROUND_HOME_DELIVERY' => 'Ground Home Delivery', + 'INTERNATIONAL_ECONOMY' => 'International Economy', + 'INTERNATIONAL_ECONOMY_FREIGHT' => 'International Economy Freight', + 'INTERNATIONAL_FIRST' => 'International First', + 'INTERNATIONAL_PRIORITY' => 'International Priority', + 'INTERNATIONAL_PRIORITY_FREIGHT' => 'International Priority Freight', + 'PRIORITY_OVERNIGHT' => 'Priority Overnight', + 'SMART_POST' => 'Smart Post', + 'STANDARD_OVERNIGHT' => 'Standard Overnight', + ]; + + public function __construct($options = []) + { + parent::__construct($options); + + if (isset($options['key'])) { + $this->key = $options['key']; + } + + if (isset($options['password'])) { + $this->password = $options['password']; + } + + if (isset($options['account_number'])) { + $this->account_number = $options['account_number']; + } + + if (isset($options['meter_number'])) { + $this->meter_number = $options['meter_number']; + } + + if (isset($options['approved_codes'])) { + $this->approved_codes = $options['approved_codes']; + } + + if (isset($options['drop_off_type'])) { + $this->drop_off_type = $options['drop_off_type']; + } + + if (isset($options['request_adapter'])) { + $this->set_request_adapter($options['request_adapter']); + } else { + $this->set_request_adapter(new RateRequest\Post()); + } + } + + protected function prepare() + { + $to = Arr::get($this->shipment, 'to'); + $shipper = Arr::get($this->shipment, 'from'); + $dimensions = Arr::get($this->shipment, 'dimensions'); + + $pounds = (int) Arr::get($this->shipment, 'weight'); + $ounces = 0; + + if ($pounds < 1) { + throw new Exception('Weight missing'); + } + + $date = time(); + $day_name = date('l', $date); + + if ($day_name == 'Saturday') { + $date += 172800; + } elseif ($day_name == 'Sunday') { + $date += 86400; + } + + // http://www.fedex.com/templates/components/apps/wpor/secure/downloads/pdf/Aug13/PropDevGuide.pdf + // http://www.fedex.com/us/developer/product/WebServices/MyWebHelp_August2010/Content/Proprietary_Developer_Guide/Rate_Services_conditionalized.htm + + $this->data = +' + + + + + + ' . $this->key . ' + ' . $this->password . ' + + + + ' . $this->account_number . ' + ' . $this->meter_number . ' + + + crs + 13 + 0 + 0 + + true + + ' . date('c') . ' + ' . $this->drop_off_type . ' + ' . Arr::get($this->shipment, 'packaging_type') . ' + +
+ ' . Arr::get($shipper, 'postal_code') . ' + ' . Arr::get($shipper, 'country_code') . ' + ' . ((Arr::get($shipper, 'is_residential')) ? '1' : '') . ' +
+
+ +
+ ' . Arr::get($to, 'postal_code') . ' + ' . Arr::get($to, 'country_code') . ' + ' . ((Arr::get($to, 'is_residential')) ? '1' : '') . ' +
+
+ LIST + 1 + + 1 + 1 + + LB + ' . $pounds . ' + + + ' . Arr::get($dimensions, 'length') . ' + ' . Arr::get($dimensions, 'width') . ' + ' . Arr::get($dimensions, 'height') . ' + IN + + +
+
+
+
'; + + return $this; + } + + protected function execute() + { + if ($this->is_prod) { + $url = $this->url_prod; + } else { + $url = $this->url_dev; + } + + $this->response = $this->rate_request->execute($url, $this->data); + + return $this; + } + + protected function process() + { + try { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->loadXml($this->response); + $rate_reply = $dom->getElementsByTagName('RateReplyDetails'); + + if (empty($rate_reply->length)) { + throw new Exception('Unable to get FedEx Rates.'); + } + } catch (Exception $e) { + // StatsD::increment('error.shipping.get_fedex_rate'); + // Kohana::$log->add(Log::ERROR, $e)->write(); + throw $e; + } + + foreach ($rate_reply as $rate) { + $code = $rate->getElementsByTagName('ServiceType')->item(0)->nodeValue; + + if ( ! empty($this->approved_codes) AND ! in_array($code, $this->approved_codes)) { + continue; + } + + $name = Arr::get($this->shipping_codes, $code); + + $delivery_ts = @$rate->getElementsByTagName('DeliveryTimestamp')->item(0)->nodeValue; + $transit_time = @$rate->getElementsByTagName('TransitTime')->item(0)->nodeValue; + + $cost = $rate + ->getElementsByTagName('RatedShipmentDetails')->item(0) + ->getElementsByTagName('ShipmentRateDetail')->item(0) + ->getElementsByTagName('TotalNetCharge')->item(0) + ->getElementsByTagName('Amount')->item(0)->nodeValue; + + $this->rates[] = array( + 'code' => $code, + 'name' => $name, + 'cost' => (int) $cost * 100, + 'delivery_ts' => $delivery_ts, + 'transit_time' => $transit_time, + ); + } + + return $this; + } +} diff --git a/src/RateAdapter.php b/src/RateAdapter.php new file mode 100644 index 0000000..430305b --- /dev/null +++ b/src/RateAdapter.php @@ -0,0 +1,64 @@ +is_prod = (bool) $options['prod']; + } + + if (isset($options['shipment'])) { + $this->shipment = $options['shipment']; + } + + if (empty($this->shipment['to'])) { + throw new Exception('Shipment "to" missing'); + } + + if (empty($this->shipment['from'])) { + throw new Exception('Shipment "from" missing'); + } + + if (empty($this->shipment['dimensions'])) { + throw new Exception('Shipment "dimensions" missing'); + } + } + + public function set_request_adapter(RateRequest\Adapter $rate_request) + { + $this->rate_request = $rate_request; + } + + public function get_rates() + { + $this + ->prepare() + ->execute() + ->process() + ->sort_by_cost(); + + return $this->rates; + } + + protected function sort_by_cost() + { + uasort($this->rates, create_function('$a, $b', 'return ($a["cost"] > $b["cost"]);')); + } +} diff --git a/src/RateRequest/Adapter.php b/src/RateRequest/Adapter.php new file mode 100644 index 0000000..1e432ef --- /dev/null +++ b/src/RateRequest/Adapter.php @@ -0,0 +1,10 @@ +curl_connect_timeout_ms); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->curl_dl_timeout); + $response = curl_exec($ch); + curl_close($ch); + + return $response; + } +} diff --git a/src/RateRequest/Post.php b/src/RateRequest/Post.php new file mode 100644 index 0000000..af3b24c --- /dev/null +++ b/src/RateRequest/Post.php @@ -0,0 +1,23 @@ +curl_connect_timeout_ms); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->curl_dl_timeout); + $response = curl_exec($ch); + curl_close($ch); + + return $response; + } +} diff --git a/src/RateRequest/StubFedex.php b/src/RateRequest/StubFedex.php new file mode 100644 index 0000000..89170ad --- /dev/null +++ b/src/RateRequest/StubFedex.php @@ -0,0 +1,23 @@ +artificial_delay = $artificial_delay; + } + + public function execute($url, $data = NULL) + { + if ($this->artificial_delay > 0) { + sleep($this->artificial_delay); + } + + $response = 'NOTENOTEcrs819The origin state/province code has been changed. The origin state/province code has been changed. NOTEcrs820The destination state/province code has been changed. The destination state/province code has been changed. crs1300PRIORITY_OVERNIGHTYOUR_PACKAGINGACTA FRI2014-09-26T12:00:00PRIORITY_OVERNIGHT2014-09-26T12:00:00FRIAM0NON_DOCUMENTSAUSfalseA1AMSERVICE_DEFAULTPAYOR_ACCOUNT_PACKAGEUSD44.63PAYOR_ACCOUNT_PACKAGE1PACKAGEDIM1949.5LB4.0LB4.0USD31.25USD0.0USD31.25USD10.63USD41.88USD0.0USD41.88USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.630USD44.63PAYOR_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD31.25USD0.0USD31.25USD10.63USD41.88USD0.0USD41.88USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.63USD44.63RATED_ACCOUNT_PACKAGE1PACKAGEDIM1949.5LB4.0LB4.0USD31.25USD0.0USD31.25USD10.63USD41.88USD0.0USD41.88USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.630USD44.63RATED_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD31.25USD0.0USD31.25USD10.63USD41.88USD0.0USD41.88USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.63PAYOR_LIST_PACKAGE15746PACKAGEDIM1669.5LB5.0LB5.0USD72.0USD0.0USD72.0USD14.51USD86.51USD0.0USD86.51USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD7.510PAYOR_LIST_PACKAGEDIMLB5.0LB5.0USD72.0USD0.0USD72.0USD14.51USD86.51USD0.0USD86.51USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD7.51RATED_LIST_PACKAGE15746PACKAGEDIM1669.5LB5.0LB5.0USD72.0USD0.0USD72.0USD14.51USD86.51USD0.0USD86.51USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD7.510RATED_LIST_PACKAGEDIMLB5.0LB5.0USD72.0USD0.0USD72.0USD14.51USD86.51USD0.0USD86.51USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD7.51STANDARD_OVERNIGHTYOUR_PACKAGINGACTA FRI2014-09-26T20:00:00STANDARD_OVERNIGHT2014-09-26T20:00:00FRIAM0NON_DOCUMENTSAUSfalseA1AMSERVICE_DEFAULTPAYOR_ACCOUNT_PACKAGEUSD4.71PAYOR_ACCOUNT_PACKAGE13716PACKAGEDIM1949.5LB4.0LB4.0USD64.55USD0.0USD64.55USD13.8USD78.35USD0.0USD78.35USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD6.80USD4.71PAYOR_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD64.55USD0.0USD64.55USD13.8USD78.35USD0.0USD78.35USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD6.8USD4.71RATED_ACCOUNT_PACKAGE13716PACKAGEDIM1949.5LB4.0LB4.0USD64.55USD0.0USD64.55USD13.8USD78.35USD0.0USD78.35USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD6.80USD4.71RATED_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD64.55USD0.0USD64.55USD13.8USD78.35USD0.0USD78.35USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD6.8PAYOR_LIST_PACKAGE13716PACKAGEDIM1669.5LB5.0LB5.0USD68.85USD0.0USD68.85USD14.21USD83.06USD0.0USD83.06USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD7.210PAYOR_LIST_PACKAGEDIMLB5.0LB5.0USD68.85USD0.0USD68.85USD14.21USD83.06USD0.0USD83.06USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD7.21RATED_LIST_PACKAGE13716PACKAGEDIM1669.5LB5.0LB5.0USD68.85USD0.0USD68.85USD14.21USD83.06USD0.0USD83.06USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD7.210RATED_LIST_PACKAGEDIMLB5.0LB5.0USD68.85USD0.0USD68.85USD14.21USD83.06USD0.0USD83.06USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD7.21FEDEX_2_DAY_AMYOUR_PACKAGINGACTA MON2014-09-29T12:00:00FEDEX_2_DAY_AM2014-09-29T12:00:00MONAM0NON_DOCUMENTSAUSfalseA1AMSERVICE_DEFAULTPAYOR_ACCOUNT_PACKAGEUSD4.42PAYOR_ACCOUNT_PACKAGE126PACKAGEDIM1949.5LB4.0LB4.0USD34.73USD0.0USD34.73USD10.96USD45.69USD0.0USD45.69USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.960USD4.42PAYOR_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD34.73USD0.0USD34.73USD10.96USD45.69USD0.0USD45.69USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.96USD4.42RATED_ACCOUNT_PACKAGE126PACKAGEDIM1949.5LB4.0LB4.0USD34.73USD0.0USD34.73USD10.96USD45.69USD0.0USD45.69USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.960USD4.42RATED_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD34.73USD0.0USD34.73USD10.96USD45.69USD0.0USD45.69USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.96PAYOR_LIST_PACKAGE126PACKAGEDIM1669.5LB5.0LB5.0USD38.76USD0.0USD38.76USD11.35USD50.11USD0.0USD50.11USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD4.350PAYOR_LIST_PACKAGEDIMLB5.0LB5.0USD38.76USD0.0USD38.76USD11.35USD50.11USD0.0USD50.11USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD4.35RATED_LIST_PACKAGE126PACKAGEDIM1669.5LB5.0LB5.0USD38.76USD0.0USD38.76USD11.35USD50.11USD0.0USD50.11USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD4.350RATED_LIST_PACKAGEDIMLB5.0LB5.0USD38.76USD0.0USD38.76USD11.35USD50.11USD0.0USD50.11USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD4.35FEDEX_2_DAYYOUR_PACKAGINGACTA MON2014-09-29T20:00:00FEDEX_2_DAY2014-09-29T20:00:00MONAM0NON_DOCUMENTSAUSfalseA1AMSERVICE_DEFAULTPAYOR_ACCOUNT_PACKAGEUSD3.84PAYOR_ACCOUNT_PACKAGE60686PACKAGEDIM1949.5LB4.0LB4.0USD30.2USD0.0USD30.2USD10.53USD40.73USD0.0USD40.73USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.530USD3.84PAYOR_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD30.2USD0.0USD30.2USD10.53USD40.73USD0.0USD40.73USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.53USD3.84RATED_ACCOUNT_PACKAGE60686PACKAGEDIM1949.5LB4.0LB4.0USD30.2USD0.0USD30.2USD10.53USD40.73USD0.0USD40.73USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.530USD3.84RATED_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD30.2USD0.0USD30.2USD10.53USD40.73USD0.0USD40.73USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.53PAYOR_LIST_PACKAGE60686PACKAGEDIM1669.5LB5.0LB5.0USD33.7USD0.0USD33.7USD10.87USD44.57USD0.0USD44.57USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.870PAYOR_LIST_PACKAGEDIMLB5.0LB5.0USD33.7USD0.0USD33.7USD10.87USD44.57USD0.0USD44.57USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.87RATED_LIST_PACKAGE60686PACKAGEDIM1669.5LB5.0LB5.0USD33.7USD0.0USD33.7USD10.87USD44.57USD0.0USD44.57USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.870RATED_LIST_PACKAGEDIMLB5.0LB5.0USD33.7USD0.0USD33.7USD10.87USD44.57USD0.0USD44.57USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD3.87FEDEX_EXPRESS_SAVERYOUR_PACKAGINGACTA TUE2014-09-30T20:00:00FEDEX_EXPRESS_SAVER2014-09-30T20:00:00TUEAM0NON_DOCUMENTSAUSfalseA1AMSERVICE_DEFAULTPAYOR_ACCOUNT_PACKAGEUSD3.01PAYOR_ACCOUNT_PACKAGE71756PACKAGEDIM1949.5LB4.0LB4.0USD20.3USD0.0USD20.3USD9.59USD29.89USD0.0USD29.89USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD2.590USD3.01PAYOR_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD20.3USD0.0USD20.3USD9.59USD29.89USD0.0USD29.89USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD2.59USD3.01RATED_ACCOUNT_PACKAGE71756PACKAGEDIM1949.5LB4.0LB4.0USD20.3USD0.0USD20.3USD9.59USD29.89USD0.0USD29.89USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD2.590USD3.01RATED_ACCOUNT_PACKAGEDIMLB4.0LB4.0USD20.3USD0.0USD20.3USD9.59USD29.89USD0.0USD29.89USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD2.59PAYOR_LIST_PACKAGE71756PACKAGEDIM1669.5LB5.0LB5.0USD23.05USD0.0USD23.05USD9.85USD32.9USD0.0USD32.9USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD2.850PAYOR_LIST_PACKAGEDIMLB5.0LB5.0USD23.05USD0.0USD23.05USD9.85USD32.9USD0.0USD32.9USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD2.85RATED_LIST_PACKAGE71756PACKAGEDIM1669.5LB5.0LB5.0USD23.05USD0.0USD23.05USD9.85USD32.9USD0.0USD32.9USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD2.850RATED_LIST_PACKAGEDIMLB5.0LB5.0USD23.05USD0.0USD23.05USD9.85USD32.9USD0.0USD32.9USD0.0RESIDENTIAL_DELIVERYResidential deliveryUSD3.35DELIVERY_AREADelivery Area Surcharge Extended ResidentialUSD3.65FUELFuelUSD2.85GROUND_HOME_DELIVERYYOUR_PACKAGINGACTA GROUND_HOME_DELIVERYTHREE_DAYS0AUSfalseA1AMTHREE_DAYSSERVICE_DEFAULTPAYOR_ACCOUNT_PACKAGEUSD0.0PAYOR_ACCOUNT_PACKAGE6ACTUAL06.5LB3.0USD9.01USD0.0USD9.01USD7.54USD16.55USD0.0USD16.55USD0.0RESIDENTIAL_DELIVERYPACKAGEFedEx Home Delivery Urban/Rural ChargeUSD2.9DELIVERY_AREAPACKAGEDelivery Area Surcharge Extended ResidentialUSD3.62FUELPACKAGEFedEx Ground FuelUSD1.020USD0.0PAYOR_ACCOUNT_PACKAGEACTUALLB3.0USD9.01USD0.0USD9.01USD7.54USD16.55USD0.0USD16.55USD0.0RESIDENTIAL_DELIVERYPACKAGEFedEx Home Delivery Urban/Rural ChargeUSD2.9DELIVERY_AREAPACKAGEDelivery Area Surcharge Extended ResidentialUSD3.62FUELPACKAGEFedEx Ground FuelUSD1.02PAYOR_LIST_PACKAGE6ACTUAL06.5LB3.0USD9.01USD0.0USD9.01USD7.54USD16.55USD0.0USD16.55USD0.0RESIDENTIAL_DELIVERYPACKAGEFedEx Home Delivery Urban/Rural ChargeUSD2.9DELIVERY_AREAPACKAGEDelivery Area Surcharge Extended ResidentialUSD3.62FUELPACKAGEFedEx Ground FuelUSD1.020PAYOR_LIST_PACKAGEACTUALLB3.0USD9.01USD0.0USD9.01USD7.54USD16.55USD0.0USD16.55USD0.0RESIDENTIAL_DELIVERYPACKAGEFedEx Home Delivery Urban/Rural ChargeUSD2.9DELIVERY_AREAPACKAGEDelivery Area Surcharge Extended ResidentialUSD3.62FUELPACKAGEFedEx Ground FuelUSD1.02'; + + return $response; + } +} diff --git a/src/RateRequest/StubUPS.php b/src/RateRequest/StubUPS.php new file mode 100644 index 0000000..67f16e3 --- /dev/null +++ b/src/RateRequest/StubUPS.php @@ -0,0 +1,24 @@ +artificial_delay = $artificial_delay; + } + + public function execute($url, $data = NULL) + { + if ($this->artificial_delay > 0) { + sleep($this->artificial_delay); + } + + $response = ' +1Success03Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS3.0USD19.10USD0.00USD19.10USD19.10USD0.00USD19.103.0LBS3.012Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS5.0USD37.18USD0.00USD37.183USD37.18USD0.00USD37.183.0LBS5.002Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS5.0USD49.23USD0.00USD49.232USD49.23USD0.00USD49.233.0LBS5.013Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS5.0USD89.54USD0.00USD89.541USD89.54USD0.00USD89.543.0LBS5.001Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS5.0USD93.28USD0.00USD93.28112:00 NoonUSD93.28USD0.00USD93.283.0LBS5.0'; + + return $response; + } +} diff --git a/src/RateRequest/StubUSPS.php b/src/RateRequest/StubUSPS.php new file mode 100644 index 0000000..b879abf --- /dev/null +++ b/src/RateRequest/StubUSPS.php @@ -0,0 +1,24 @@ +artificial_delay = $artificial_delay; + } + + public function execute($url, $data = NULL) + { + if ($this->artificial_delay > 0) { + sleep($this->artificial_delay); + } + + $response = ' +904017666730LARGEFALSE6Priority Mail Express 2-Day&lt;sup&gt;&#8482;&lt;/sup&gt;42.25Priority Mail Express 2-Day&lt;sup&gt;&#8482;&lt;/sup&gt; Hold For Pickup42.25Priority Mail 2-Day&lt;sup&gt;&#8482;&lt;/sup&gt;12.20Standard Post&lt;sup&gt;&#174;&lt;/sup&gt;10.01Media Mail Parcel3.65Library Mail Parcel3.48'; + + return $response; + } +} diff --git a/src/Ship.php b/src/Ship.php new file mode 100644 index 0000000..c68941f --- /dev/null +++ b/src/Ship.php @@ -0,0 +1,143 @@ + [ + 'ups' => [ + '03' => '1-5 business days', + ], + 'fedex' => [ + 'FEDEX_EXPRESS_SAVER' => '1-3 business days', + 'FEDEX_GROUND' => '1-5 business days', + 'GROUND_HOME_DELIVERY' => '1-5 business days', + ], + 'usps' => [ + '1' => '1-3 business days', + '4' => '2-8 business days', + ], + ], + 'Two-Day Shipping' => [ + 'ups' => [ + '02' => '2 business days', + ], + 'fedex' => [ + 'FEDEX_2_DAY' => '2 business days', + ], + ], + 'One-Day Shipping' => [ + 'ups' => [ + '01' => 'next business day 10:30am', + '13' => 'next business day by 3pm', + '14' => 'next business day by 8am', + ], + 'fedex' => [ + 'STANDARD_OVERNIGHT' => 'overnight', + ], + ], + ]; + + public static function factory($shipping_options = []) + { + $object = new self(); + + if ( ! empty($shipping_options)) { + $object->shipping_options = $shipping_options; + } + + return $object; + } + + public function get_approved_codes($carrier = NULL) { + $approved_codes = []; + + // Build approved_codes + foreach ($this->shipping_options as $shipping_group => $row) { + + foreach ($row as $_carrier => $row2) { + if ( ! isset($approved_codes[$_carrier])) { + $approved_codes[$_carrier] = []; + } + + foreach ($row2 as $code => $display) { + $approved_codes[$_carrier][] = $code; + } + } + } + + if ($carrier !== NULL AND isset($approved_codes[$carrier])) { + return $approved_codes[$carrier]; + } + + return $approved_codes; + } + + public function get_display_rates($rates) + { + // Build output array with cheapest shipping option for each group + $display_rates = []; + foreach ($this->shipping_options as $shipping_group => $row) { + $display_rates[$shipping_group] = []; + $cheapest_row = NULL; + + foreach ($row as $carrier => $row2) { + $group_codes = array_keys($row2); + + if ( ! empty($rates[$carrier])) { + + foreach ($rates[$carrier] as $row3) { + + if (in_array($row3['code'], $group_codes)) { + $row3['carrier'] = $carrier; + + if ($cheapest_row === NULL) { + $cheapest_row = $row3; + } else { + if ($row3['cost'] < $cheapest_row['cost']) { + $cheapest_row = $row3; + } + } + } + } + } + } + $display_rates[$shipping_group][] = $cheapest_row; + } + + return $display_rates; + } + + public function get_all_display_rates($rates) + { + // Build output array listing all group options + $display_rates = []; + foreach ($this->shipping_options as $shipping_group => $row) { + $display_rates[$shipping_group] = []; + + foreach ($row as $carrier => $row2) { + $group_codes = array_keys($row2); + + if ( ! empty($rates[$carrier])) { + + foreach ($rates[$carrier] as $row3) { + + if (in_array($row3['code'], $group_codes)) { + $row3['carrier'] = $carrier; + $display_rates[$shipping_group][] = $row3; + } + } + } + } + + $this->sort_by_cost($display_rates[$shipping_group]); + } + + return $display_rates; + } + + protected function sort_by_cost( & $rates) + { + uasort($rates, create_function('$a, $b', 'return ($a["cost"] > $b["cost"]);')); + } +} diff --git a/src/UPS/Rate.php b/src/UPS/Rate.php new file mode 100644 index 0000000..d51f09a --- /dev/null +++ b/src/UPS/Rate.php @@ -0,0 +1,247 @@ + [ // United States + '01' => 'UPS Next Day Air', + '02' => 'UPS 2nd Day Air', + '03' => 'UPS Ground', + '07' => 'UPS Worldwide Express', + '08' => 'UPS Worldwide Expedited', + '11' => 'UPS Standard', + '12' => 'UPS 3 Day Select', + '13' => 'UPS Next Day Air Saver', + '14' => 'UPS Next Day Air Early A.M.', + '54' => 'UPS Worldwide Express Plus', + '59' => 'UPS 2nd Day Air A.M.', + '65' => 'UPS Saver', + ], + 'CA' => [ // Canada + '01' => 'UPS Express', + '02' => 'UPS Expedited', + '07' => 'UPS Worldwide Express', + '08' => 'UPS Worldwide Expedited', + '11' => 'UPS Standard', + '12' => 'UPS 3 Day Select', + '13' => 'UPS Saver', + '14' => 'UPS Express Early A.M.', + '54' => 'UPS Worldwide Express Plus', + '65' => 'UPS Saver', + ], + 'EU' => [ // European Union + '07' => 'UPS Express', + '08' => 'UPS Expedited', + '11' => 'UPS Standard', + '54' => 'UPS Worldwide Express Plus', + '65' => 'UPS Saver', + '82' => 'UPS Today Standard', + '83' => 'UPS Today Dedicated Courier', + '84' => 'UPS Today Intercity', + '85' => 'UPS Today Express', + '86' => 'UPS Today Express Saver', + '01' => 'UPS Next Day Air', + '02' => 'UPS 2nd Day Air', + '03' => 'UPS Ground', + '14' => 'UPS Next Day Air Early A.M.', + ], + 'MX' => [ // Mexico + '07' => 'UPS Express', + '08' => 'UPS Expedited', + '54' => 'UPS Express Plus', + '65' => 'UPS Saver', + ], + 'other' => [ // Other + '07' => 'UPS Express', + '08' => 'UPS Worldwide Expedited', + '11' => 'UPS Standard', + '54' => 'UPS Worldwide Express Plus', + '65' => 'UPS Saver', + ], + ]; + + public function __construct($options = []) + { + parent::__construct($options); + + if (isset($options['access_key'])) { + $this->access_key = $options['access_key']; + } + + if (isset($options['user_id'])) { + $this->user_id = $options['user_id']; + } + + if (isset($options['password'])) { + $this->password = $options['password']; + } + + if (isset($options['shipper_number'])) { + $this->shipper_number = $options['shipper_number']; + } + + if (isset($options['approved_codes'])) { + $this->approved_codes = $options['approved_codes']; + } + + if (isset($options['request_adapter'])) { + $this->set_request_adapter($options['request_adapter']); + } else { + $this->set_request_adapter(new RateRequest\Post()); + } + } + + protected function prepare() + { + $to = Arr::get($this->shipment, 'to'); + $shipper = Arr::get($this->shipment, 'from'); + $dimensions = Arr::get($this->shipment, 'dimensions'); + + $pounds = (int) Arr::get($this->shipment, 'weight'); + $ounces = 0; + + if ($pounds < 1) { + throw new Exception('Weight missing'); + } + + $service_code = '03'; + + $this->data = +' + + ' . $this->access_key . ' + ' . $this->user_id . ' + ' . $this->password . ' + + + + Rate + shop + + + +
+ ' . Arr::get($shipper, 'postal_code') . ' + ' . Arr::get($shipper, 'country_code') . ' + ' . ((Arr::get($shipper, 'is_residential')) ? '1' : '') . ' +
+ ' . $this->shipper_number . ' +
+ +
+ ' . Arr::get($to, 'postal_code') . ' + ' . Arr::get($to, 'country_code') . ' + ' . ((Arr::get($to, 'is_residential')) ? '1' : '') . ' +
+
+ +
+ ' . Arr::get($shipper, 'postal_code') . ' + ' . Arr::get($shipper, 'country_code') . ' + ' . ((Arr::get($shipper, 'is_residential')) ? '1' : '') . ' +
+
+ + ' . $service_code . ' + + + + 02 + + + + IN + + ' . Arr::get($dimensions, 'length') . ' + ' . Arr::get($dimensions, 'width') . ' + ' . Arr::get($dimensions, 'height') . ' + + + + LBS + + ' . $pounds . ' + + +
+
'; + + return $this; + } + + protected function execute() + { + if ($this->is_prod) { + $url = $this->url_prod; + } else { + $url = $this->url_dev; + } + + $this->response = $this->rate_request->execute($url, $this->data); + + return $this; + } + + protected function process() + { + try { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->loadXml($this->response); + + $rate_list = @$dom->getElementsByTagName('RatedShipment'); + + if (empty($rate_list->length)) { + throw new Exception('Unable to get UPS Rates.'); + } + } catch (Exception $e) { + // StatsD::increment('error.shipping.get_ups_rate'); + // Kohana::$log->add(Log::ERROR, $e)->write(); + throw $e; + } + + foreach ($rate_list as $rate) { + $code = @$rate + ->getElementsByTagName('Service')->item(0) + ->getElementsByTagName('Code')->item(0)->nodeValue; + + $name = Arr::get($this->shipping_codes['US'], $code); + + $cost = @$rate + ->getElementsByTagName('TotalCharges')->item(0) + ->getElementsByTagName('MonetaryValue')->item(0)->nodeValue; + + if ( ! empty($this->approved_codes) AND ! in_array($code, $this->approved_codes)) { + continue; + } + + $this->rates[] = array( + 'code' => $code, + 'name' => $name, + 'cost' => (int) $cost * 100, + ); + } + + return $this; + } +} diff --git a/src/USPS/Rate.php b/src/USPS/Rate.php new file mode 100644 index 0000000..c54351d --- /dev/null +++ b/src/USPS/Rate.php @@ -0,0 +1,182 @@ + [ + '00' => 'First-Class Mail Parcel', + '01' => 'First-Class Mail Large Envelope', + '02' => 'First-Class Mail Letter', + '03' => 'First-Class Mail Postcards', + '1' => 'Priority Mail', + '2' => 'Express Mail Hold for Pickup', + '3' => 'Express Mail', + '4' => 'Parcel Post', // Standard Post + '5' => 'Bound Printed Matter', + '6' => 'Media Mail', + '7' => 'Library', + '12' => 'First-Class Postcard Stamped', + '13' => 'Express Mail Flat-Rate Envelope', + '16' => 'Priority Mail Flat-Rate Envelope', + '17' => 'Priority Mail Regular Flat-Rate Box', + '18' => 'Priority Mail Keys and IDs', + '19' => 'First-Class Keys and IDs', + '22' => 'Priority Mail Flat-Rate Large Box', + '23' => 'Express Mail Sunday/Holiday', + '25' => 'Express Mail Flat-Rate Envelope Sunday/Holiday', + '27' => 'Express Mail Flat-Rate Envelope Hold For Pickup', + '28' => 'Priority Mail Small Flat-Rate Box', + ], + 'international' => [ + '1' => 'Express Mail International', + '2' => 'Priority Mail International', + '4' => 'Global Express Guaranteed (Document and Non-document)', + '5' => 'Global Express Guaranteed Document used', + '6' => 'Global Express Guaranteed Non-Document Rectangular shape', + '7' => 'Global Express Guaranteed Non-Document Non-Rectangular', + '8' => 'Priority Mail Flat Rate Envelope', + '9' => 'Priority Mail Flat Rate Box', + '10' => 'Express Mail International Flat Rate Envelope', + '11' => 'Priority Mail Flat Rate Large Box', + '12' => 'Global Express Guaranteed Envelope', + '13' => 'First Class Mail International Letters', + '14' => 'First Class Mail International Flats', + '15' => 'First Class Mail International Parcels', + '16' => 'Priority Mail Flat Rate Small Box', + '21' => 'Postcards', + ], + ]; + + public function __construct($options = []) + { + parent::__construct($options); + + if (isset($options['username'])) { + $this->username = $options['username']; + } + + if (isset($options['password'])) { + $this->password = $options['password']; + } + + if (isset($options['username'])) { + $this->username = $options['username']; + } + + if (isset($options['approved_codes'])) { + $this->approved_codes = $options['approved_codes']; + } + + if (isset($options['request_adapter'])) { + $this->set_request_adapter($options['request_adapter']); + } else { + $this->set_request_adapter(new RateRequest\Get()); + } + } + + protected function prepare() + { + $to = Arr::get($this->shipment, 'to'); + $shipper = Arr::get($this->shipment, 'from'); + $dimensions = Arr::get($this->shipment, 'dimensions'); + + // https://www.usps.com/business/web-tools-apis/rate-calculators-v1-7a.htm + $pounds = (int) Arr::get($this->shipment, 'weight'); + $ounces = 0; + + if ($pounds < 1) { + throw new Exception('Weight missing'); + } + + $this->data = +' + + + ALL + ' . Arr::get($shipper, 'postal_code') . ' + ' . Arr::get($to, 'postal_code') . ' + ' . $pounds . ' + ' . $ounces . ' + ' . Arr::get($this->shipment, 'container') . ' + ' . Arr::get($this->shipment, 'size') . ' + ' . Arr::get($dimensions, 'width') . ' + ' . Arr::get($dimensions, 'length') . ' + ' . Arr::get($dimensions, 'height') . ' + ' . 'False' . ' + +'; + + return $this; + } + + protected function execute() + { + if ($this->is_prod) { + $url = $this->url_prod; + } else { + $url = $this->url_dev; + } + + $url_request = $url . '?API=RateV4&XML=' . rawurlencode($this->data); + + $this->response = $this->rate_request->execute($url_request); + + return $this; + } + + protected function process() + { + try { + $dom = new DOMDocument('1.0', 'UTF-8'); + $dom->loadXml($this->response); + + $postage_list = @$dom->getElementsByTagName('Postage'); + + if (empty($postage_list)) { + throw new Exception('Unable to get USPS Rates.'); + } + } catch (Exception $e) { + // StatsD::increment('error.shipping.get_usps_rate'); + // Kohana::$log->add(Log::ERROR, $e)->write(); + throw $e; + } + + foreach ($postage_list as $postage) { + $code = @$postage->getAttribute('CLASSID'); + $cost = @$postage->getElementsByTagName('Rate')->item(0)->nodeValue; + + $name = Arr::get($this->shipping_codes['domestic'], $code); + + if ( ! empty($this->approved_codes) AND ! in_array($code, $this->approved_codes)) { + continue; + } + + $this->rates[] = array( + 'code' => $code, + 'name' => $name, + 'cost' => (int) $cost * 100, + ); + } + + return $this; + } +} diff --git a/tests/ShipTest.php b/tests/ShipTest.php new file mode 100644 index 0000000..9467d7f --- /dev/null +++ b/tests/ShipTest.php @@ -0,0 +1,425 @@ + 3, // lbs + 'dimensions' => [ + 'width' => 9, + 'length' => 9, + 'height' => 9, + ], + 'from' => [ + 'postal_code' => '90401', + 'country_code' => 'US', + ], + 'to' => [ + 'postal_code' => '78703', + 'country_code' => 'US', + 'is_residential' => TRUE, + ], + ]; + + public $shipping_options = [ + 'Standard Shipping' => [ + 'ups' => [ + '03' => '1-5 business days', + ], + 'fedex' => [ + 'FEDEX_EXPRESS_SAVER' => '1-3 business days', + 'FEDEX_GROUND' => '1-5 business days', + 'GROUND_HOME_DELIVERY' => '1-5 business days', + ], + 'usps' => [ + '1' => '1-3 business days', + '4' => '2-8 business days', + ], + ], + 'Two-Day Shipping' => [ + 'ups' => [ + '02' => '2 business days', + ], + 'fedex' => [ + 'FEDEX_2_DAY' => '2 business days', + ], + ], + 'One-Day Shipping' => [ + 'ups' => [ + '01' => 'next business day 10:30am', + '13' => 'next business day by 3pm', + '14' => 'next business day by 8am', + ], + 'fedex' => [ + 'STANDARD_OVERNIGHT' => 'overnight', + ], + ], + ]; + + private function getUSPSOptions() + { + $ship = Ship::factory($this->shipping_options); + $approved_codes = $ship->get_approved_codes('usps'); + + return [ + 'prod' => FALSE, + 'username' => 'XXXX', + 'password' => 'XXXX', + 'shipment' => array_merge($this->shipment, [ + 'size' => 'LARGE', + 'container' => 'RECTANGULAR', + ]), + 'approved_codes' => $approved_codes, + 'request_adapter' => new RateRequest\StubUSPS(), + ]; + } + + private function getUPSOptions() + { + $ship = Ship::factory($this->shipping_options); + $approved_codes = $ship->get_approved_codes('ups'); + + return [ + 'prod' => FALSE, + 'access_key' => 'XXXX', + 'user_id' => 'XXXX', + 'password' => 'XXXX', + 'shipper_number' => 'XXXX', + 'shipment' => $this->shipment, + 'approved_codes' => $approved_codes, + 'request_adapter' => new RateRequest\StubUPS(), + ]; + } + + private function getFedexOptions() + { + $ship = Ship::factory($this->shipping_options); + $approved_codes = $ship->get_approved_codes('fedex'); + + return [ + 'prod' => FALSE, + 'key' => 'XXXX', + 'password' => 'XXXX', + 'account_number' => 'XXXX', + 'meter_number' => 'XXXX', + 'drop_off_type' => 'BUSINESS_SERVICE_CENTER', + 'shipment' => array_merge($this->shipment, [ + 'packaging_type' => 'YOUR_PACKAGING', + ]), + 'approved_codes' => $approved_codes, + 'request_adapter' => new RateRequest\StubFedex(), + ]; + } + + public function testUSPSRate() + { + $usps = new USPS\Rate($this->getUSPSOptions()); + $usps_rates = $usps->get_rates(); + + $this->assertEquals(json_encode([ + 1 => [ + 'code' => '4', + 'name' => 'Parcel Post', + 'cost' => 1000, + ], + 0 => [ + 'code' => '1', + 'name' => 'Priority Mail', + 'cost' => 1200, + ], + ]), json_encode($usps_rates)); + } + + public function testUPSRate() + { + $ups = new UPS\Rate($this->getUPSOptions()); + $ups_rates = $ups->get_rates(); + + $this->assertEquals(json_encode([ + 0 => [ + 'code' => '03', + 'name' => 'UPS Ground', + 'cost' => 1900, + ], + 1 => [ + 'code' => '02', + 'name' => 'UPS 2nd Day Air', + 'cost' => 4900, + ], + 2 => [ + 'code' => '13', + 'name' => 'UPS Next Day Air Saver', + 'cost' => 8900, + ], + 3 => [ + 'code' => '01', + 'name' => 'UPS Next Day Air', + 'cost' => 9300, + ], + ]), json_encode($ups_rates)); + } + + public function testFedexRate() + { + $fedex = new Fedex\Rate($this->getFedexOptions()); + $fedex_rates = $fedex->get_rates(); + + $this->assertEquals(json_encode([ + 3 => [ + 'code' => 'GROUND_HOME_DELIVERY', + 'name' => 'Ground Home Delivery', + 'cost' => 1600, + 'delivery_ts' => NULL, + 'transit_time' => 'THREE_DAYS', + ], + 2 => [ + 'code' => 'FEDEX_EXPRESS_SAVER', + 'name' => 'Fedex Express Saver', + 'cost' => 2900, + 'delivery_ts' => '2014-09-30T20:00:00', + 'transit_time' => NULL, + ], + 1 => [ + 'code' => 'FEDEX_2_DAY', + 'name' => 'Fedex 2 Day', + 'cost' => 4000, + 'delivery_ts' => '2014-09-29T20:00:00', + 'transit_time' => NULL, + ], + 0 => [ + 'code' => 'STANDARD_OVERNIGHT', + 'name' => 'Standard Overnight', + 'cost' => 7800, + 'delivery_ts' => '2014-09-26T20:00:00', + 'transit_time' => NULL, + ], + ]), json_encode($fedex_rates)); + } + + public function testDisplayOptions() + { + $rates = []; + + $usps = new USPS\Rate($this->getUSPSOptions()); + $rates['usps'] = $usps->get_rates(); + + $ups = new UPS\Rate($this->getUPSOptions()); + $rates['ups'] = $ups->get_rates(); + + $fedex = new Fedex\Rate($this->getFedexOptions()); + $rates['fedex'] = $fedex->get_rates(); + + $ship = Ship::factory($this->shipping_options); + $display_rates = $ship->get_display_rates($rates); + + $this->assertEquals(json_encode([ + 'Standard Shipping' => [ + 0 => [ + 'code' => '4', + 'name' => 'Parcel Post', + 'cost' => 1000, + 'carrier' => 'usps', + ], + ], + 'Two-Day Shipping' => [ + 0 => [ + 'code' => 'FEDEX_2_DAY', + 'name' => 'Fedex 2 Day', + 'cost' => 4000, + 'delivery_ts' => '2014-09-29T20:00:00', + 'transit_time' => NULL, + 'carrier' => 'fedex', + ], + ], + 'One-Day Shipping' => [ + 0 => [ + 'code' => 'STANDARD_OVERNIGHT', + 'name' => 'Standard Overnight', + 'cost' => 7800, + 'delivery_ts' => '2014-09-26T20:00:00', + 'transit_time' => NULL, + 'carrier' => 'fedex', + ], + ], + ]), json_encode($display_rates)); + } + + /** + * @expectedException Exception + */ + public function testUSPSRateMissingTo() + { + $usps_options = $this->getUSPSOptions(); + unset($usps_options['shipment']['to']); + + $usps = new USPS\Rate($usps_options); + $usps_rates = $usps->get_rates(); + } + + /** + * @expectedException Exception + */ + public function testUSPSRateMissingFrom() + { + $usps_options = $this->getUSPSOptions(); + unset($usps_options['shipment']['from']); + + $usps = new USPS\Rate($usps_options); + $usps_rates = $usps->get_rates(); + } + + /** + * @expectedException Exception + */ + public function testUSPSRateMissingDimensions() + { + $usps_options = $this->getUSPSOptions(); + unset($usps_options['shipment']['dimensions']); + + $usps = new USPS\Rate($usps_options); + $usps_rates = $usps->get_rates(); + } + + /** + * @expectedException Exception + */ + public function testUPSRateMissingTo() + { + $ups_options = $this->getUPSOptions(); + unset($ups_options['shipment']['to']); + + $ups = new UPS\Rate($ups_options); + $ups_rates = $ups->get_rates(); + } + + /** + * @expectedException Exception + */ + public function testUPSRateMissingFrom() + { + $ups_options = $this->getUPSOptions(); + unset($ups_options['shipment']['from']); + + $ups = new UPS\Rate($ups_options); + $ups_rates = $ups->get_rates(); + } + + /** + * @expectedException Exception + */ + public function testUPSRateMissingDimensions() + { + $ups_options = $this->getUPSOptions(); + unset($ups_options['shipment']['dimensions']); + + $ups = new UPS\Rate($ups_options); + $ups_rates = $ups->get_rates(); + } + + /** + * @expectedException Exception + */ + public function testFedexRateMissingTo() + { + $fedex_options = $this->getFedexOptions(); + unset($fedex_options['shipment']['to']); + + $fedex = new Fedex\Rate($fedex_options); + $fedex_rates = $fedex->get_rates(); + } + + /** + * @expectedException Exception + */ + public function testFedexRateMissingFrom() + { + $fedex_options = $this->getFedexOptions(); + unset($fedex_options['shipment']['from']); + + $fedex = new Fedex\Rate($fedex_options); + $fedex_rates = $fedex->get_rates(); + } + + /** + * @expectedException Exception + */ + public function testFedexRateMissingDimensions() + { + $fedex_options = $this->getFedexOptions(); + unset($fedex_options['shipment']['dimensions']); + + $fedex = new Fedex\Rate($fedex_options); + $fedex_rates = $fedex->get_rates(); + } + + // // Readme Examples: + // public function testUSPSReadmeExample() + // { + // $usps = new USPS\Rate([ + // 'prod' => FALSE, + // 'username' => 'XXXX', + // 'password' => 'XXXX', + // 'shipment' => array_merge($this->shipment, [ + // 'size' => 'LARGE', + // 'container' => 'RECTANGULAR', + // ]), + // 'approved_codes' => [ + // '1', // 1-3 business days + // '4', // 2-8 business days + // ], + // 'request_adapter' => new RateRequest\StubUSPS(), + // ]); + // + // $usps_rates = $usps->get_rates(); + // var_export($usps_rates); + // } + // + // public function testUPSReadmeExample() + // { + // $ups = new UPS\Rate([ + // 'prod' => FALSE, + // 'shipment' => $this->shipment, + // 'approved_codes' => [ + // '03', // 1-5 business days + // '02', // 2 business days + // '01', // next business day 10:30am + // '13', // next business day by 3pm + // '14', // next business day by 8am + // ], + // 'request_adapter' => new RateRequest\StubUPS(), + // ]); + // + // $ups_rates = $ups->get_rates(); + // var_export($ups_rates); + // } + // + // public function testFedexReadmeExample() + // { + // $fedex = new Fedex\Rate([ + // 'prod' => FALSE, + // 'key' => 'XXXX', + // 'password' => 'XXXX', + // 'account_number' => 'XXXX', + // 'meter_number' => 'XXXX', + // 'drop_off_type' => 'BUSINESS_SERVICE_CENTER', + // 'shipment' => array_merge($this->shipment, [ + // 'packaging_type' => 'YOUR_PACKAGING', + // ]), + // 'approved_codes' => [ + // 'FEDEX_EXPRESS_SAVER', // 1-3 business days + // 'FEDEX_GROUND', // 1-5 business days + // 'GROUND_HOME_DELIVERY', // 1-5 business days + // 'FEDEX_2_DAY', // 2 business days + // 'STANDARD_OVERNIGHT', // overnight + // ], + // 'request_adapter' => new RateRequest\StubFedex(), + // ]); + // + // $fedex_rates = $fedex->get_rates(); + // var_export($fedex_rates); + // } +}