Browse Source

Migrating code to PSR2. Adding optional support for live API calls in unit tests. Adding rounding error bug fix. Adding more tests to get to 85% coverage. Adding phpstorm project. Bumping master for v1.0.x target.

pull/11/head
Jamie Isaacs 7 years ago
parent
commit
ccf447f755
  1. 11
      .gitignore
  2. 1
      .idea/.name
  3. 5
      .idea/encodings.xml
  4. 10
      .idea/inspectionProfiles/Project_Default.xml
  5. 7
      .idea/inspectionProfiles/profiles_settings.xml
  6. 5
      .idea/misc.xml
  7. 9
      .idea/modules.xml
  8. 9
      .idea/php.xml
  9. 5
      .idea/scopes/scope_settings.xml
  10. 7
      .idea/vcs.xml
  11. 10
      .travis.yml
  12. 145
      README.md
  13. 11
      composer.json
  14. 16
      live_phpunit.sh.example
  15. 28
      phpunit.xml
  16. 8
      src/Arr.php
  17. 440
      src/Fedex/Rate.php
  18. 36
      src/Quote.php
  19. 99
      src/RateAdapter.php
  20. 6
      src/RateRequest/Adapter.php
  21. 3440
      src/RateRequest/FedexResponse.xml
  22. 37
      src/RateRequest/Get.php
  23. 39
      src/RateRequest/Post.php
  24. 6
      src/RateRequest/RequestException.php
  25. 26
      src/RateRequest/StubFedex.php
  26. 27
      src/RateRequest/StubUPS.php
  27. 27
      src/RateRequest/StubUSPS.php
  28. 247
      src/RateRequest/UPSResponse.xml
  29. 37
      src/RateRequest/USPSResponse.xml
  30. 286
      src/Ship.php
  31. 75
      src/Shipment.php
  32. 469
      src/UPS/Rate.php
  33. 387
      src/USPS/Rate.php
  34. 28
      tests/ArrTest.php
  35. 110
      tests/Fedex/FedexTest.php
  36. 19
      tests/PackageTest.php
  37. 25
      tests/QuoteTest.php
  38. 13
      tests/RateAdapterTest.php
  39. 424
      tests/ShipTest.php
  40. 32
      tests/ShipmentTest.php
  41. 99
      tests/UPS/RateTest.php
  42. 86
      tests/USPS/RateTest.php

11
.gitignore

@ -1,4 +1,9 @@
vendor/
.idea/
vendor
composer.lock
coverage_report
.idea/*.iws
.idea/*.iml
.idea/workspace.xml
.idea/tasks.xml
.DS_Store
live_phpunit.sh

1
.idea/.name

@ -0,0 +1 @@
shipping

5
.idea/encodings.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" />
</project>

10
.idea/inspectionProfiles/Project_Default.xml

@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0" is_locked="false">
<option name="myName" value="Project Default" />
<option name="myLocal" value="false" />
<inspection_tool class="PhpCSValidationInspection" enabled="true" level="ERROR" enabled_by_default="true">
<option name="CODING_STANDARD" value="PSR2" />
<option name="WARNING_HIGHLIGHT_LEVEL_NAME" value="ERROR" />
</inspection_tool>
</profile>
</component>

7
.idea/inspectionProfiles/profiles_settings.xml

@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Project Default" />
<option name="USE_PROJECT_PROFILE" value="true" />
<version value="1.0" />
</settings>
</component>

5
.idea/misc.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" />
</project>

9
.idea/modules.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/shipping.iml" filepath="$PROJECT_DIR$/.idea/shipping.iml" />
</modules>
</component>
</project>

9
.idea/php.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings load_method="CUSTOM_LOADER" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" phpunit_phar_path="" />
</phpunit_settings>
</component>
</project>

5
.idea/scopes/scope_settings.xml

@ -0,0 +1,5 @@
<component name="DependencyValidationManager">
<state>
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</state>
</component>

7
.idea/vcs.xml

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

10
.travis.yml

@ -0,0 +1,10 @@
language: php
php:
- 5.4
before_script:
- composer install --prefer-dist
script:
- vendor/bin/phpcs --standard=PSR2 src/ tests/
- vendor/bin/phpunit

145
README.md

@ -1,18 +1,31 @@
## PHP Shipping API
[![Total Downloads](https://poser.pugx.org/pdt256/shipping/downloads.svg)](https://packagist.org/packages/pdt256/shipping) [![Latest Stable Version](https://poser.pugx.org/pdt256/shipping/v/stable.svg)](https://packagist.org/packages/pdt256/shipping) [![Latest Unstable Version](https://poser.pugx.org/pdt256/shipping/v/unstable.svg)](https://packagist.org/packages/pdt256/shipping) [![License](https://poser.pugx.org/pdt256/shipping/license.svg)](https://packagist.org/packages/pdt256/shipping)
[![Test Coverage](http://img.shields.io/badge/coverage-85%25-green.svg)]
[![Build Status](https://travis-ci.org/pdt256/shipping.svg?branch=master)](https://travis-ci.org/pdt256/shipping)
[![Downloads](https://img.shields.io/packagist/dt/pdt256/shipping.svg)](https://packagist.org/packages/pdt256/shipping)
[![License](https://img.shields.io/packagist/l/pdt256/shipping.svg)](https://github.com/pdt256/shipping/blob/master/LICENSE.txt)
A shipping rate wrapper for USPS, UPS, and Fedex.
## Introduction
This is a PHP shipping package that wraps API calls to UPS, FedEx, and USPS for shipping rates.
Multiple packages can be added to get additional rates.
All code (including tests) conform to the PSR-2 coding standards.
The namespace and autoloader are using the PSR-4 standard.
All pull requests are processed by Travis CI to conform to PSR-2 and to verify all unit tests pass.
## Installation
Add the following lines to your ``composer.json`` file.
```JSON
{
"require": {
"pdt256/shipping": "dev-master"
}
"require": {
"pdt256/shipping": "1.0.*@dev"
}
}
```
@ -23,15 +36,20 @@ Create a shipment object:
```php
$shipment = new Shipment;
$shipment
->setFromIsResidential(false)
->setFromStateProvinceCode('IN')
->setFromPostalCode('46205')
->setFromCountryCode('US')
->setToIsResidential(true);
->setToPostalCode('20101')
->setToCountryCode('US')
->setToResidential(true);
$package = new Package;
$package->setLength(12)->setWidth(4)->setHeight(3)->setWeight(3);
$package
->setLength(12)
->setWidth(4)
->setHeight(3)
->setWeight(3);
$shipment->addPackage($package);
```
@ -44,7 +62,7 @@ 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.
```php
'request_adapter' => new RateRequest\StubUPS(),
'requestAdapter' => new RateRequest\StubUPS(),
```
```php
@ -52,23 +70,23 @@ 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(),
'prod' => FALSE,
'accessKey' => 'XXXX',
'userId' => 'XXXX',
'password' => 'XXXX',
'shipperNumber' => 'XXXX',
'shipment' => $shipment,
'approvedCodes' => [
'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
],
'requestAdapter' => new RateRequest\StubUPS(),
]);
$ups_rates = $ups->get_rates();
$upsRates = $ups->getRates();
```
Output array sorted by cost: (in cents)
@ -83,9 +101,9 @@ array(4) {
string(10) "UPS Ground"
protected $cost =>
int(1900)
protected $transit_time =>
protected $transitTime =>
NULL
protected $delivery_ts =>
protected $deliveryTime =>
NULL
protected $carrier =>
string(3) "ups"
@ -98,9 +116,9 @@ array(4) {
string(15) "UPS 2nd Day Air"
protected $cost =>
int(4900)
protected $transit_time =>
protected $transitTime =>
NULL
protected $delivery_ts =>
protected $deliveryTime =>
NULL
protected $carrier =>
string(3) "ups"
@ -113,9 +131,9 @@ array(4) {
string(22) "UPS Next Day Air Saver"
protected $cost =>
int(8900)
protected $transit_time =>
protected $transitTime =>
NULL
protected $delivery_ts =>
protected $deliveryTime =>
NULL
protected $carrier =>
string(3) "ups"
@ -128,9 +146,9 @@ array(4) {
string(16) "UPS Next Day Air"
protected $cost =>
int(9300)
protected $transit_time =>
protected $transitTime =>
NULL
protected $delivery_ts =>
protected $deliveryTime =>
NULL
protected $carrier =>
string(3) "ups"
@ -149,14 +167,14 @@ $usps = new USPS\Rate([
'username' => 'XXXX',
'password' => 'XXXX',
'shipment' => $shipment,
'approved_codes' => [
'approvedCodes' => [
'1', // 1-3 business days
'4', // 2-8 business days
],
'request_adapter' => new RateRequest\StubUSPS(),
'requestAdapter' => new RateRequest\StubUSPS(),
]);
$usps_rates = $usps->get_rates();
$uspsRates = $usps->getRates();
```
Output array sorted by cost: (in cents)
@ -171,9 +189,9 @@ array(2) {
string(11) "Parcel Post"
protected $cost =>
int(1001)
protected $transit_time =>
protected $transitTime =>
NULL
protected $delivery_ts =>
protected $deliveryTime =>
NULL
protected $carrier =>
string(4) "usps"
@ -186,9 +204,9 @@ array(2) {
string(13) "Priority Mail"
protected $cost =>
int(1220)
protected $transit_time =>
protected $transitTime =>
NULL
protected $delivery_ts =>
protected $deliveryTime =>
NULL
protected $carrier =>
string(4) "usps"
@ -206,21 +224,21 @@ $fedex = new Fedex\Rate([
'prod' => FALSE,
'key' => 'XXXX',
'password' => 'XXXX',
'account_number' => 'XXXX',
'meter_number' => 'XXXX',
'drop_off_type' => 'BUSINESS_SERVICE_CENTER',
'accountNumber' => 'XXXX',
'meterNumber' => 'XXXX',
'dropOffType' => 'BUSINESS_SERVICE_CENTER',
'shipment' => $shipment,
'approved_codes' => [
'approvedCodes' => [
'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(),
'requestAdapter' => new RateRequest\StubFedex(),
]);
$fedex_rates = $fedex->get_rates();
$fedexRates = $fedex->getRates();
```
Output array sorted by cost: (in cents)
@ -235,9 +253,9 @@ array(4) {
string(20) "Ground Home Delivery"
protected $cost =>
int(1600)
protected $transit_time =>
protected $transitTime =>
string(10) "THREE_DAYS"
protected $delivery_ts =>
protected $deliveryTime =>
NULL
protected $carrier =>
string(5) "fedex"
@ -250,9 +268,9 @@ array(4) {
string(19) "Fedex Express Saver"
protected $cost =>
int(2900)
protected $transit_time =>
protected $transitTime =>
NULL
protected $delivery_ts =>
protected $deliveryTime =>
class Carbon\Carbon#23 (3) {
public $date =>
string(26) "2014-09-30 20:00:00.000000"
@ -272,9 +290,9 @@ array(4) {
string(11) "Fedex 2 Day"
protected $cost =>
int(4000)
protected $transit_time =>
protected $transitTime =>
NULL
protected $delivery_ts =>
protected $deliveryTime =>
class Carbon\Carbon#26 (3) {
public $date =>
string(26) "2014-09-29 20:00:00.000000"
@ -294,9 +312,9 @@ array(4) {
string(18) "Standard Overnight"
protected $cost =>
int(7800)
protected $transit_time =>
protected $transitTime =>
NULL
protected $delivery_ts =>
protected $deliveryTime =>
class Carbon\Carbon#58 (3) {
public $date =>
string(26) "2014-09-26 20:00:00.000000"
@ -311,6 +329,31 @@ array(4) {
}
```
## Unit Tests:
```bash
vendor/bin/phpunit
```
### With Code Coverage:
```bash
vendor/bin/phpunit --coverage-text --coverage-html coverage_report
```
### With Live API Tests:
```bash
./live_phpunit.sh
```
## Run Coding Standards Test:
```bash
vendor/bin/phpcs --standard=PSR2 src/ tests/
```
### License
The MIT License (MIT)

11
composer.json

@ -9,8 +9,17 @@
"email": "pdt256@gmail.com"
}
],
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"require-dev": {
"phpunit/phpunit": "4.0.*"
"phpunit/phpunit": "4.0.*",
"squizlabs/php_codesniffer": "1.5.5"
},
"require": {
"php": ">=5.4"
},
"autoload": {
"psr-4": {

16
live_phpunit.sh.example

@ -0,0 +1,16 @@
#!/bin/bash
export USPS_USERNAME='XXXXXXXXXXXX'
export USPS_PASSWORD='XXXXXXXXXXXX'
export UPS_ACCESS_KEY='XXXXXXXXXXXXXXXX'
export UPS_USER_ID='XXXXXX'
export UPS_PASSWORD='XXXX'
export UPS_SHIPPER_NUMBER='XXXXXX'
export FEDEX_KEY='XXXXXXXXXXXXXXXX'
export FEDEX_PASSWORD='XXXXXXXXXXXXXXXXXXXXXXXXX'
export FEDEX_ACCOUNT_NUMBER='XXXXXXXXX'
export FEDEX_METER_NUMBER='XXXXXXXXX'
vendor/bin/phpunit

28
phpunit.xml

@ -1,17 +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>
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>

8
src/Arr.php

@ -3,8 +3,8 @@ namespace pdt256\Shipping;
class Arr
{
public static function get($array, $key, $default = NULL)
{
return isset($array[$key]) ? $array[$key] : $default;
}
public static function get($array, $key, $default = null)
{
return isset($array[$key]) ? $array[$key] : $default;
}
}

440
src/Fedex/Rate.php

@ -12,230 +12,218 @@ 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()
{
$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
$packages = '';
$sequence_number = 0;
foreach ($this->shipment->getPackages() as $p) {
$sequence_number++;
$packages .= '<RequestedPackageLineItems>
<SequenceNumber>' . $sequence_number . '</SequenceNumber>
<GroupPackageCount>1</GroupPackageCount>
<Weight>
<Units>LB</Units>
<Value>' . $p->getWeight() . '</Value>
</Weight>
<Dimensions>
<Length>' . $p->getLength() . '</Length>
<Width>' . $p->getWidth() . '</Width>
<Height>' . $p->getHeight() . '</Height>
<Units>IN</Units>
</Dimensions>
</RequestedPackageLineItems>';
}
$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>YOUR_PACKAGING</PackagingType>
<Shipper>
<Address>
<PostalCode>' . $this->shipment->getFromPostalCode() . '</PostalCode>
<CountryCode>' . $this->shipment->getFromCountryCode() . '</CountryCode>
' . (($this->shipment->isFromResidential()) ? '<Residential>1</Residential>' : '') . '
</Address>
</Shipper>
<Recipient>
<Address>
<PostalCode>' . $this->shipment->getToPostalCode() . '</PostalCode>
<CountryCode>' . $this->shipment->getToCountryCode() . '</CountryCode>
' . (($this->shipment->isToResidential()) ? '<Residential>1</Residential>' : '') . '
</Address>
</Recipient>
<RateRequestTypes>LIST</RateRequestTypes>
<PackageCount>' . $this->shipment->packageCount() . '</PackageCount>
' . $packages . '
</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;
$quote = new Quote;
$quote
->setCarrier('fedex')
->setCode($code)
->setName($name)
->setCost((int) $cost * 100)
->setTransitTime($transit_time);
if ($delivery_ts) {
$quote->setDeliveryEstimate(new DateTime($delivery_ts));
}
$this->rates[] = $quote;
}
return $this;
}
private $urlDev = 'https://gatewaybeta.fedex.com/web-services/';
private $urlProd = 'https://gateway.fedex.com/web-services/';
private $key = 'XXX';
private $password = 'XXX';
private $accountNumber = 'XXX';
private $meterNumber = 'XXX';
private $dropOffType = 'BUSINESS_SERVICE_CENTER';
public $approvedCodes = [
'PRIORITY_OVERNIGHT',
'FEDEX_2_DAY',
'FEDEX_EXPRESS_SAVER',
'FEDEX_GROUND',
'GROUND_HOME_DELIVERY',
];
private $shippingCodes = [
'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);
$this->key = Arr::get($options, 'key');
$this->password = Arr::get($options, 'password');
$this->accountNumber = Arr::get($options, 'accountNumber');
$this->meterNumber = Arr::get($options, 'meterNumber');
$this->approvedCodes = Arr::get($options, 'approvedCodes');
$this->dropOffType = Arr::get($options, 'dropOffType');
$this->setRequestAdapter(Arr::get($options, 'requestAdapter', new RateRequest\Post()));
}
protected function prepare()
{
$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
$packages = '';
$sequence_number = 0;
foreach ($this->shipment->getPackages() as $package) {
$sequence_number++;
$packages .=
'<RequestedPackageLineItems>' .
'<SequenceNumber>' . $sequence_number . '</SequenceNumber>' .
'<GroupPackageCount>1</GroupPackageCount>' .
'<Weight>' .
'<Units>LB</Units>' .
'<Value>' . $package->getWeight() . '</Value>' .
'</Weight>' .
'<Dimensions>' .
'<Length>' . $package->getLength() . '</Length>' .
'<Width>' . $package->getWidth() . '</Width>' .
'<Height>' . $package->getHeight() . '</Height>' .
'<Units>IN</Units>' .
'</Dimensions>' .
'</RequestedPackageLineItems>';
}
$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->accountNumber . '</AccountNumber>' .
'<MeterNumber>' . $this->meterNumber . '</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->dropOffType . '</DropoffType>' .
'<PackagingType>YOUR_PACKAGING</PackagingType>' .
'<Shipper>' .
'<Address>' .
'<PostalCode>' . $this->shipment->getFromPostalCode() . '</PostalCode>' .
'<CountryCode>' . $this->shipment->getFromCountryCode() . '</CountryCode>' .
(
$this->shipment->getFromIsResidential() ?
'<Residential>1</Residential>' :
''
) .
'</Address>' .
'</Shipper>' .
'<Recipient>' .
'<Address>' .
'<PostalCode>' . $this->shipment->getToPostalCode() . '</PostalCode>' .
'<CountryCode>' . $this->shipment->getToCountryCode() . '</CountryCode>' .
(
$this->shipment->getToIsResidential() ?
'<Residential>1</Residential>' :
''
) .
'</Address>' .
'</Recipient>' .
'<RateRequestTypes>LIST</RateRequestTypes>' .
'<PackageCount>' . $this->shipment->packageCount() . '</PackageCount>' .
$packages .
'</RequestedShipment>' .
'</RateRequest>' .
'</SOAP-ENV:Body>' .
'</SOAP-ENV:Envelope>';
return $this;
}
protected function execute()
{
if ($this->isProduction) {
$url = $this->urlProd;
} else {
$url = $this->urlDev;
}
$this->response = $this->rateRequest->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->approvedCodes) && ! in_array($code, $this->approvedCodes)) {
continue;
}
$name = Arr::get($this->shippingCodes, $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;
$quote = new Quote;
$quote
->setCarrier('fedex')
->setCode($code)
->setName($name)
->setCost((int) ($cost * 100))
->setTransitTime($transit_time);
if ($delivery_ts) {
$quote->setDeliveryEstimate(new DateTime($delivery_ts));
}
$this->rates[] = $quote;
}
return $this;
}
}

36
src/Quote.php

@ -7,10 +7,18 @@ class Quote
protected $code;
protected $name;
protected $cost;
protected $transit_time;
protected $delivery_ts;
protected $transitTime;
protected $deliveryEstimate;
protected $carrier;
public function __construct($carrier = null, $code = null, $name = null, $cost = null)
{
$this->setCarrier($carrier);
$this->setCode($code);
$this->setName($name);
$this->setCost($cost);
}
/**
* @return mixed
*/
@ -25,7 +33,7 @@ class Quote
*/
public function setCarrier($carrier)
{
$this->carrier = $carrier;
$this->carrier = (string) $carrier;
return $this;
}
@ -43,7 +51,7 @@ class Quote
*/
public function setCode($code)
{
$this->code = $code;
$this->code = (string) $code;
return $this;
}
@ -61,7 +69,7 @@ class Quote
*/
public function setName($name)
{
$this->name = $name;
$this->name = (string) $name;
return $this;
}
@ -81,7 +89,7 @@ class Quote
*/
public function setCost($cost)
{
$this->cost = $cost;
$this->cost = (int) $cost;
return $this;
}
@ -90,16 +98,16 @@ class Quote
*/
public function getTransitTime()
{
return $this->transit_time;
return $this->transitTime;
}
/**
* @param mixed $transit_time
* @param mixed $transitTime
* @return $this
*/
public function setTransitTime($transit_time)
public function setTransitTime($transitTime)
{
$this->transit_time = $transit_time;
$this->transitTime = $transitTime;
return $this;
}
@ -108,16 +116,16 @@ class Quote
*/
public function getDeliveryEstimate()
{
return $this->delivery_ts;
return $this->deliveryEstimate;
}
/**
* @param DateTime $estimate
* @param DateTime $deliveryEstimate
* @return $this
*/
public function setDeliveryEstimate(DateTime $estimate)
public function setDeliveryEstimate(DateTime $deliveryEstimate)
{
$this->delivery_ts = $estimate;
$this->deliveryEstimate = $deliveryEstimate;
return $this;
}
}

99
src/RateAdapter.php

@ -5,49 +5,58 @@ use Exception;
abstract class RateAdapter
{
protected $is_prod = FALSE;
/** @var Shipment */
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'];
}
}
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 array_values($this->rates);
}
protected function sort_by_cost()
{
uasort($this->rates, create_function('$a, $b', 'return ($a->getCost() > $b->getCost());'));
}
protected $isProduction;
/** @var Shipment */
protected $shipment;
protected $data;
protected $response;
protected $rates;
/** @var @var RateRequest\Adapter */
protected $rateRequest;
/**
* Prepare XML
*/
abstract protected function prepare();
/**
* Curl Request
*/
abstract protected function execute();
/**
* Convert to shipping rates array
*/
abstract protected function process();
public function __construct($options = [])
{
$this->rates = [];
$this->isProduction = (bool) Arr::get($options, 'prod', false);
$this->shipment = Arr::get($options, 'shipment');
}
public function setRequestAdapter(RateRequest\Adapter $rateRequest)
{
$this->rateRequest = $rateRequest;
}
public function getRates()
{
$this
->prepare()
->execute()
->process()
->sortByCost();
return array_values($this->rates);
}
protected function sortByCost()
{
uasort($this->rates, create_function('$a, $b', 'return ($a->getCost() > $b->getCost());'));
}
}

6
src/RateRequest/Adapter.php

@ -3,8 +3,8 @@ namespace pdt256\Shipping\RateRequest;
abstract class Adapter
{
protected $curl_connect_timeout_ms = 1000; // milliseconds
protected $curl_dl_timeout = 11; // seconds
protected $curlConnectTimeoutInMilliseconds = 1000;
protected $curlDownloadTimeoutInSeconds = 11;
abstract public function execute($url, $data = NULL);
abstract public function execute($url, $data = null);
}

3440
src/RateRequest/FedexResponse.xml
File diff suppressed because it is too large
View File

37
src/RateRequest/Get.php

@ -3,22 +3,23 @@ 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);
if ($response === false) {
throw new RequestException(curl_error($ch));
}
return $response;
}
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_SSL_CIPHER_LIST, 'TLSv1');
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->curlConnectTimeoutInMilliseconds);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->curlDownloadTimeoutInSeconds);
$response = curl_exec($ch);
curl_close($ch);
if ($response === false) {
throw new RequestException(curl_error($ch));
}
return $response;
}
}

39
src/RateRequest/Post.php

@ -3,24 +3,25 @@ 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);
if ($response === false) {
throw new RequestException(curl_error($ch));
}
curl_close($ch);
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_SSL_CIPHER_LIST, 'TLSv1');
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->curlConnectTimeoutInMilliseconds);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->curlDownloadTimeoutInSeconds);
$response = curl_exec($ch);
if ($response === false) {
throw new RequestException(curl_error($ch));
}
curl_close($ch);
return $response;
}
return $response;
}
}

6
src/RateRequest/RequestException.php

@ -1,6 +1,6 @@
<?php
namespace pdt256\Shipping\RateRequest;
class RequestException extends \Exception {}
class RequestException extends \Exception
{
}

26
src/RateRequest/StubFedex.php
File diff suppressed because it is too large
View File

27
src/RateRequest/StubUPS.php
File diff suppressed because it is too large
View File

27
src/RateRequest/StubUSPS.php

@ -3,22 +3,19 @@ namespace pdt256\Shipping\RateRequest;
class StubUSPS extends Adapter
{
private $artificial_delay = 0;
private $artificialDelay = 0;
public function __construct($artificial_delay = 0)
{
$this->artificial_delay = $artificial_delay;
}
public function __construct($artificial_delay = 0)
{
$this->artificialDelay = $artificial_delay;
}
public function execute($url, $data = NULL)
{
if ($this->artificial_delay > 0) {
sleep($this->artificial_delay);
}
public function execute($url, $data = null)
{
if ($this->artificialDelay > 0) {
sleep($this->artificialDelay);
}
$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;
}
return file_get_contents(__DIR__ . '/USPSResponse.xml');
}
}

247
src/RateRequest/UPSResponse.xml

@ -0,0 +1,247 @@
<?xml version="1.0"?>
<RatingServiceSelectionResponse>
<Response>
<ResponseStatusCode>1</ResponseStatusCode>
<ResponseStatusDescription>Success</ResponseStatusDescription>
</Response>
<RatedShipment>
<Service>
<Code>03</Code>
</Service>
<RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning>
<RatedShipmentWarning>Missing / Invalid Shipper Number. Returned rates are Retail Rates.</RatedShipmentWarning>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>3.0</Weight>
</BillingWeight>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>19.10</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>19.10</MonetaryValue>
</TotalCharges>
<GuaranteedDaysToDelivery/>
<ScheduledDeliveryTime/>
<RatedPackage>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>19.10</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>19.10</MonetaryValue>
</TotalCharges>
<Weight>3.0</Weight>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>3.0</Weight>
</BillingWeight>
</RatedPackage>
</RatedShipment>
<RatedShipment>
<Service>
<Code>12</Code>
</Service>
<RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning>
<RatedShipmentWarning>Missing / Invalid Shipper Number. Returned rates are Retail Rates.</RatedShipmentWarning>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>5.0</Weight>
</BillingWeight>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>37.18</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>37.18</MonetaryValue>
</TotalCharges>
<GuaranteedDaysToDelivery>3</GuaranteedDaysToDelivery>
<ScheduledDeliveryTime/>
<RatedPackage>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>37.18</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>37.18</MonetaryValue>
</TotalCharges>
<Weight>3.0</Weight>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>5.0</Weight>
</BillingWeight>
</RatedPackage>
</RatedShipment>
<RatedShipment>
<Service>
<Code>02</Code>
</Service>
<RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning>
<RatedShipmentWarning>Missing / Invalid Shipper Number. Returned rates are Retail Rates.</RatedShipmentWarning>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>5.0</Weight>
</BillingWeight>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>49.23</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>49.23</MonetaryValue>
</TotalCharges>
<GuaranteedDaysToDelivery>2</GuaranteedDaysToDelivery>
<ScheduledDeliveryTime/>
<RatedPackage>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>49.23</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>49.23</MonetaryValue>
</TotalCharges>
<Weight>3.0</Weight>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>5.0</Weight>
</BillingWeight>
</RatedPackage>
</RatedShipment>
<RatedShipment>
<Service>
<Code>13</Code>
</Service>
<RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning>
<RatedShipmentWarning>Missing / Invalid Shipper Number. Returned rates are Retail Rates.</RatedShipmentWarning>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>5.0</Weight>
</BillingWeight>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>89.54</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>89.54</MonetaryValue>
</TotalCharges>
<GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery>
<ScheduledDeliveryTime/>
<RatedPackage>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>89.54</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>89.54</MonetaryValue>
</TotalCharges>
<Weight>3.0</Weight>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>5.0</Weight>
</BillingWeight>
</RatedPackage>
</RatedShipment>
<RatedShipment>
<Service>
<Code>01</Code>
</Service>
<RatedShipmentWarning>Your invoice may vary from the displayed reference rates</RatedShipmentWarning>
<RatedShipmentWarning>Missing / Invalid Shipper Number. Returned rates are Retail Rates.</RatedShipmentWarning>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>5.0</Weight>
</BillingWeight>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>93.28</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>93.28</MonetaryValue>
</TotalCharges>
<GuaranteedDaysToDelivery>1</GuaranteedDaysToDelivery>
<ScheduledDeliveryTime>12:00 Noon</ScheduledDeliveryTime>
<RatedPackage>
<TransportationCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>93.28</MonetaryValue>
</TransportationCharges>
<ServiceOptionsCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>0.00</MonetaryValue>
</ServiceOptionsCharges>
<TotalCharges>
<CurrencyCode>USD</CurrencyCode>
<MonetaryValue>93.28</MonetaryValue>
</TotalCharges>
<Weight>3.0</Weight>
<BillingWeight>
<UnitOfMeasurement>
<Code>LBS</Code>
</UnitOfMeasurement>
<Weight>5.0</Weight>
</BillingWeight>
</RatedPackage>
</RatedShipment>
</RatingServiceSelectionResponse>

37
src/RateRequest/USPSResponse.xml

@ -0,0 +1,37 @@
<?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>

286
src/Ship.php

@ -3,145 +3,149 @@ namespace pdt256\Shipping;
class Ship
{
protected $shipping_options = [
'Standard Shipping' => [