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 = 'NOTENOTE819
The origin state/province code has been changed. The origin state/province code has been changed. NOTE820
The 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 = '
+1Success03
Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS
3.0USD19.10USD0.00USD19.10USD19.10USD0.00USD19.103.0LBS
3.012
Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS
5.0USD37.18USD0.00USD37.183USD37.18USD0.00USD37.183.0LBS
5.002
Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS
5.0USD49.23USD0.00USD49.232USD49.23USD0.00USD49.233.0LBS
5.013
Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS
5.0USD89.54USD0.00USD89.541USD89.54USD0.00USD89.543.0LBS
5.001
Your invoice may vary from the displayed reference ratesMissing / Invalid Shipper Number. Returned rates are Retail Rates.LBS
5.0USD93.28USD0.00USD93.28112:00 NoonUSD93.28USD0.00USD93.283.0LBS
5.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<sup>™</sup>42.25Priority Mail Express 2-Day<sup>™</sup> Hold For Pickup42.25Priority Mail 2-Day<sup>™</sup>12.20Standard Post<sup>®</sup>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);
+ // }
+}