Adding initial shipping API version.

pull/1/head
Jamie Isaacs 10 years ago
parent a7158e2781
commit 5590fc75a4

@ -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)

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false">
<testsuites>
<testsuite name="Ship Test Suite">
<directory suffix=".php">./tests</directory>
</testsuite>
</testsuites>
</phpunit>

@ -0,0 +1,10 @@
<?php
namespace pdt256\Shipping;
class Arr
{
public static function get($array, $key, $default = NULL)
{
return isset($array[$key]) ? $array[$key] : $default;
}
}

@ -0,0 +1,236 @@
<?php
namespace pdt256\Shipping\Fedex;
use pdt256\Shipping;
use pdt256\Shipping\Arr;
use pdt256\Shipping\RateAdapter;
use pdt256\Shipping\RateRequest;
use DOMDocument;
use Exception;
class Rate extends RateAdapter
{
private $url_dev = 'https://gatewaybeta.fedex.com/web-services/';
private $url_prod = 'https://gateway.fedex.com/web-services/';
private $key = 'XXX';
private $password = 'XXX';
private $account_number = 'XXX';
private $meter_number = 'XXX';
private $drop_off_type = 'BUSINESS_SERVICE_CENTER';
public $approved_codes = [
'PRIORITY_OVERNIGHT',
'FEDEX_2_DAY',
'FEDEX_EXPRESS_SAVER',
'FEDEX_GROUND',
'GROUND_HOME_DELIVERY',
];
private $shipping_codes = [
'EUROPE_FIRST_INTERNATIONAL_PRIORITY' => '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 =
'<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns="http://fedex.com/ws/rate/v13">
<SOAP-ENV:Body>
<RateRequest>
<WebAuthenticationDetail>
<UserCredential>
<Key>' . $this->key . '</Key>
<Password>' . $this->password . '</Password>
</UserCredential>
</WebAuthenticationDetail>
<ClientDetail>
<AccountNumber>' . $this->account_number . '</AccountNumber>
<MeterNumber>' . $this->meter_number . '</MeterNumber>
</ClientDetail>
<Version>
<ServiceId>crs</ServiceId>
<Major>13</Major>
<Intermediate>0</Intermediate>
<Minor>0</Minor>
</Version>
<ReturnTransitAndCommit>true</ReturnTransitAndCommit>
<RequestedShipment>
<ShipTimestamp>' . date('c') . '</ShipTimestamp>
<DropoffType>' . $this->drop_off_type . '</DropoffType>
<PackagingType>' . Arr::get($this->shipment, 'packaging_type') . '</PackagingType>
<Shipper>
<Address>
<PostalCode>' . Arr::get($shipper, 'postal_code') . '</PostalCode>
<CountryCode>' . Arr::get($shipper, 'country_code') . '</CountryCode>
' . ((Arr::get($shipper, 'is_residential')) ? '<Residential>1</Residential>' : '') . '
</Address>
</Shipper>
<Recipient>
<Address>
<PostalCode>' . Arr::get($to, 'postal_code') . '</PostalCode>
<CountryCode>' . Arr::get($to, 'country_code') . '</CountryCode>
' . ((Arr::get($to, 'is_residential')) ? '<Residential>1</Residential>' : '') . '
</Address>
</Recipient>
<RateRequestTypes>LIST</RateRequestTypes>
<PackageCount>1</PackageCount>
<RequestedPackageLineItems>
<SequenceNumber>1</SequenceNumber>
<GroupPackageCount>1</GroupPackageCount>
<Weight>
<Units>LB</Units>
<Value>' . $pounds . '</Value>
</Weight>
<Dimensions>
<Length>' . Arr::get($dimensions, 'length') . '</Length>
<Width>' . Arr::get($dimensions, 'width') . '</Width>
<Height>' . Arr::get($dimensions, 'height') . '</Height>
<Units>IN</Units>
</Dimensions>
</RequestedPackageLineItems>
</RequestedShipment>
</RateRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>';
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;
}
}

@ -0,0 +1,64 @@
<?php
namespace pdt256\Shipping;
use Exception;
abstract class RateAdapter
{
protected $is_prod = FALSE;
protected $shipment;
protected $data;
protected $response;
protected $rates = [];
protected $rate_request;
abstract protected function prepare(); // Prepare XML
abstract protected function execute(); // Curl Request
abstract protected function process(); // Convert to shipping rates array
public function __construct($options = [])
{
if (isset($options['prod'])) {
$this->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"]);'));
}
}

@ -0,0 +1,10 @@
<?php
namespace pdt256\Shipping\RateRequest;
abstract class Adapter
{
protected $curl_connect_timeout_ms = 1000; // milliseconds
protected $curl_dl_timeout = 11; // seconds
abstract public function execute($url, $data = NULL);
}

@ -0,0 +1,22 @@
<?php
namespace pdt256\Shipping\RateRequest;
class Get extends Adapter
{
public function execute($url, $data = NULL)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_HTTPGET, TRUE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->curl_connect_timeout_ms);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->curl_dl_timeout);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}

@ -0,0 +1,23 @@
<?php
namespace pdt256\Shipping\RateRequest;
class Post extends Adapter
{
public function execute($url, $data = NULL)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->curl_connect_timeout_ms);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->curl_dl_timeout);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,24 @@
<?php
namespace pdt256\Shipping\RateRequest;
class StubUSPS extends Adapter
{
private $artificial_delay = 0;
public function __construct($artificial_delay = 0)
{
$this->artificial_delay = $artificial_delay;
}
public function execute($url, $data = NULL)
{
if ($this->artificial_delay > 0) {
sleep($this->artificial_delay);
}
$response = '<?xml version="1.0" encoding="UTF-8"?>
<RateV4Response><Package ID="1"><ZipOrigination>90401</ZipOrigination><ZipDestination>76667</ZipDestination><Pounds>3</Pounds><Ounces>0</Ounces><Size>LARGE</Size><Machinable>FALSE</Machinable><Zone>6</Zone><Postage CLASSID="3"><MailService>Priority Mail Express 2-Day&amp;lt;sup&amp;gt;&amp;#8482;&amp;lt;/sup&amp;gt;</MailService><Rate>42.25</Rate></Postage><Postage CLASSID="2"><MailService>Priority Mail Express 2-Day&amp;lt;sup&amp;gt;&amp;#8482;&amp;lt;/sup&amp;gt; Hold For Pickup</MailService><Rate>42.25</Rate></Postage><Postage CLASSID="1"><MailService>Priority Mail 2-Day&amp;lt;sup&amp;gt;&amp;#8482;&amp;lt;/sup&amp;gt;</MailService><Rate>12.20</Rate></Postage><Postage CLASSID="4"><MailService>Standard Post&amp;lt;sup&amp;gt;&amp;#174;&amp;lt;/sup&amp;gt;</MailService><Rate>10.01</Rate></Postage><Postage CLASSID="6"><MailService>Media Mail Parcel</MailService><Rate>3.65</Rate></Postage><Postage CLASSID="7"><MailService>Library Mail Parcel</MailService><Rate>3.48</Rate></Postage></Package></RateV4Response>';
return $response;
}
}

@ -0,0 +1,143 @@
<?php
namespace pdt256\Shipping;
class Ship
{
protected $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',
],
],
];
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"]);'));
}
}

@ -0,0 +1,247 @@
<?php
namespace pdt256\Shipping\UPS;
use pdt256\Ship;
use pdt256\Shipping\Arr;
use pdt256\Shipping\RateAdapter;
use pdt256\Shipping\RateRequest;
use DOMDocument;
use Exception;
class Rate extends RateAdapter
{
private $url_dev = 'https://wwwcie.ups.com/ups.app/xml/Rate';
private $url_prod = 'https://www.ups.com/ups.app/xml/Rate';
private $access_key = 'XXX';
private $user_id = 'XXX';
private $password = 'XXX';
private $shipper_number = 'XXX';
public $approved_codes = [
'03',
'12',
];
private $shipping_codes = [
'US' => [ // 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 =
'<?xml version="1.0"?>
<AccessRequest xml:lang="en-US">
<AccessLicenseNumber>' . $this->access_key . '</AccessLicenseNumber>
<UserId>' . $this->user_id . '</UserId>
<Password>' . $this->password . '</Password>
</AccessRequest>
<RatingServiceSelectionRequest xml:lang="en-US">
<Request>
<RequestAction>Rate</RequestAction>
<RequestOption>shop</RequestOption>
</Request>
<Shipment>
<Shipper>
<Address>
<PostalCode>' . Arr::get($shipper, 'postal_code') . '</PostalCode>
<CountryCode>' . Arr::get($shipper, 'country_code') . '</CountryCode>
' . ((Arr::get($shipper, 'is_residential')) ? '<ResidentialAddressIndicator>1</ResidentialAddressIndicator>' : '') . '
</Address>
<ShipperNumber>' . $this->shipper_number . '</ShipperNumber>
</Shipper>
<ShipTo>
<Address>
<PostalCode>' . Arr::get($to, 'postal_code') . '</PostalCode>
<CountryCode>' . Arr::get($to, 'country_code') . '</CountryCode>
' . ((Arr::get($to, 'is_residential')) ? '<ResidentialAddressIndicator>1</ResidentialAddressIndicator>' : '') . '
</Address>
</ShipTo>
<ShipFrom>
<Address>
<PostalCode>' . Arr::get($shipper, 'postal_code') . '</PostalCode>
<CountryCode>' . Arr::get($shipper, 'country_code') . '</CountryCode>
' . ((Arr::get($shipper, 'is_residential')) ? '<ResidentialAddressIndicator>1</ResidentialAddressIndicator>' : '') . '
</Address>
</ShipFrom>
<Service>
<Code>' . $service_code . '</Code>
</Service>
<Package>
<PackagingType>
<Code>02</Code>
</PackagingType>
<Dimensions>
<UnitOfMeasurement>
<Code>IN</Code>
</UnitOfMeasurement>
<Length>' . Arr::get($dimensions, 'length') . '</Length>
<Width>' . Arr::get($dimensions, 'width') . '</Width>
<Height>' . Arr::get($dimensions, 'height') . '</Height>
</Dimensions>
<PackageWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>' . $pounds . '</Weight>
</PackageWeight>
</Package>
</Shipment>
</RatingServiceSelectionRequest>';
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;
}
}

@ -0,0 +1,182 @@
<?php
namespace pdt256\Shipping\USPS;
use pdt256\Shipping;
use pdt256\Shipping\Arr;
use pdt256\Shipping\RateAdapter;
use pdt256\Shipping\RateRequest;
use DOMDocument;
use Exception;
class Rate extends RateAdapter
{
private $url_dev = 'http://production.shippingapis.com/ShippingAPI.dll';
private $url_prod = 'http://production.shippingapis.com/ShippingAPI.dll';
private $username = 'XXX';
private $password = 'XXX';
public $approved_codes = [
'1',
'4',
];
private $shipping_codes = [
'domestic' => [
'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 =
'<RateV4Request USERID="' . $this->username . '">
<Revision/>
<Package ID="1">
<Service>ALL</Service>
<ZipOrigination>' . Arr::get($shipper, 'postal_code') . '</ZipOrigination>
<ZipDestination>' . Arr::get($to, 'postal_code') . '</ZipDestination>
<Pounds>' . $pounds . '</Pounds>
<Ounces>' . $ounces . '</Ounces>
<Container>' . Arr::get($this->shipment, 'container') . '</Container>
<Size>' . Arr::get($this->shipment, 'size') . '</Size>
<Width>' . Arr::get($dimensions, 'width') . '</Width>
<Length>' . Arr::get($dimensions, 'length') . '</Length>
<Height>' . Arr::get($dimensions, 'height') . '</Height>
<Machinable>' . 'False' . '</Machinable>
</Package>
</RateV4Request>';
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;
}
}

@ -0,0 +1,425 @@
<?php
use pdt256\Shipping\Ship;
use pdt256\Shipping\USPS;
use pdt256\Shipping\UPS;
use pdt256\Shipping\Fedex;
use pdt256\Shipping\RateRequest;
class ShipTest extends PHPUnit_Framework_TestCase
{
public $shipment = [
'weight' => 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);
// }
}
Loading…
Cancel
Save