Merge commit 'ups-driver/master'

This commit is contained in:
Colin Kuskie 2009-11-03 20:37:53 -08:00
commit deff69ae14
6 changed files with 1708 additions and 0 deletions

View file

@ -0,0 +1,533 @@
package WebGUI::Shop::ShipDriver::UPS;
use strict;
use base qw/WebGUI::Shop::ShipDriver/;
use WebGUI::Exception;
use WebGUI::Exception::Shop;
use XML::Simple;
use LWP;
use Tie::IxHash;
use Locales::Country qw/ en /;
use Class::InsideOut qw/ :std /;
use Data::Dumper;
public testMode => my %testMode;
=head1 NAME
Package WebGUI::Shop::ShipDriver::UPS
=head1 DESCRIPTION
Shipping driver for the United Parcel Service, for US Domestic shipping only.
The UPS XML interface will only do a lookup for one destination at a time. However,
each destination may have multiple packages. This means that if a cart holds packages
with multiple destinations, that multiple requests must be sent to the UPS server.
=head1 SYNOPSIS
=head1 METHODS
See the master class, WebGUI::Shop::ShipDriver for information about
base methods. These methods are customized in this class:
=cut
#-------------------------------------------------------------------
=head2 buildXML ( $cart, @packages )
Returns XML for submitting to the UPS servers
=head3 $cart
A WebGUI::Shop::Cart object. This allows us access to the user's
address book
=head3 $packages
An array reference. Each array element is 1 set of items. The
quantity of items will vary in each set. All packages in the set must
go to the same zipcode.
=cut
sub buildXML {
my ($self, $cart, $packages) = @_;
#tie my %xmlHash, 'Tie::IxHash';
my %xmlHash = (
AccessRequest => {},
);
my $xmlAcc = $xmlHash{AccessRequest};
$xmlAcc->{'xml:lang'} = 'en-US';
$xmlAcc->{AccessLicenseNumber} = [ $self->get('licenseNo') ];
$xmlAcc->{UserId} = [ $self->get('userId') ];
$xmlAcc->{Password} = [ $self->get('password') ];
my $localizedCountry = Locales::Country->new('en');
my $xml = XMLout(\%xmlHash,
KeepRoot => 1,
NoSort => 1,
SuppressEmpty => 0,
XMLDecl => 1,
);
my $destination = $packages->[0]->[0]->getShippingAddress;
%xmlHash = (
RatingServiceSelectionRequest => {},
);
my $xmlRate = $xmlHash{RatingServiceSelectionRequest };
$xmlRate->{'xml:lang'} = 'en-US';
$xmlRate->{Request} = {
# Shown in example request, but optional
# TransactionReference => {
# CustomerContext => [ 'Rating and Service' ],
# XpciVersion => [ 1.0001 ],
# },
RequestAction => [ 'Rate' ],
# RequestOption => [ 'shop' ],
};
$xmlRate->{PickupType} = {
Code => [ $self->get('pickupType') ],
};
$xmlRate->{CustomerClassification} = {
Code => [ $self->get('customerClassification') ],
};
$xmlRate->{Shipment} = {
Shipper => {
Address => [ {
PostalCode => [ $self->get('sourceZip' ) ],
CountryCode => [ $localizedCountry->country2code($self->get('sourceCountry'), 'alpha2') ],
}, ],
},
ShipTo => {
Address => [ {
PostalCode => [ $destination->get('code') ],
CountryCode => [ $localizedCountry->country2code($destination->get('country'), 'alpha2') ],
} ],
},
Service => {
Code => [ $self->get('shipService') ],
},
Package => [],
};
if ($self->get('residentialIndicator') eq 'residential') {
$xmlRate->{Shipment}->{ShipTo}->{Address}->[0]->{ResidentialAddressIndicator} = [''];
}
my $packHash = $xmlRate->{Shipment}->{Package};
PACKAGE: foreach my $package (@{ $packages }) {
my $weight = 0;
ITEM: foreach my $item (@{ $package }) {
my $sku = $item->getSku();
next ITEM unless $sku->isShippingRequired;
##If shipsSeparately is set, the item was placed N times in the shippingBundles,
##where N is the quantity. This means that the quantity is wrong for
##any item where that option is set.
my $skuWeight = $sku->getWeight;
if (! $sku->shipsSeparately() ) {
$skuWeight *= $item->get('quantity');
}
$weight += $skuWeight;
}
next PACKAGE unless $weight;
$weight = sprintf "%.1f", $weight;
$weight = '0.1' if $weight == 0;
my $options = {
PackagingType => [ {
Code => [ '02' ],
} ],
PackageWeight => [ {
Weight => [ $weight ], ##Required formatting from spec
} ],
};
push @{ $packHash }, $options;
}
return '' unless scalar @{ $packHash }; ##Nothing to calculate shipping for.
$xml .= XMLout(\%xmlHash,
KeepRoot => 1,
NoSort => 1,
SuppressEmpty => '',
XMLDecl => 1,
);
return $xml;
}
#-------------------------------------------------------------------
=head2 calculate ( $cart )
Returns a shipping price. Since the UPS will only allow a lookup from one source
to one destination at a time, this method may make several XML requests from the
UPS server.
=head3 $cart
A WebGUI::Shop::Cart object. The contents of the cart are analyzed to calculate
the shipping costs. If no items in the cart require shipping, then no shipping
costs are assessed.
=cut
sub calculate {
my ($self, $cart) = @_;
if (! $self->get('sourceZip')) {
WebGUI::Error::InvalidParam->throw(error => q{Driver configured without a source zipcode.});
}
if (! $self->get('sourceCountry')) {
WebGUI::Error::InvalidParam->throw(error => q{Driver configured without a source country.});
}
if (! $self->get('userId')) {
WebGUI::Error::InvalidParam->throw(error => q{Driver configured without a UPS userId.});
}
if (! $self->get('password')) {
WebGUI::Error::InvalidParam->throw(error => q{Driver configured without a UPS password.});
}
if (! $self->get('licenseNo')) {
WebGUI::Error::InvalidParam->throw(error => q{Driver configured without a UPS license number.});
}
my $cost = 0;
##Sort the items into shippable bundles.
my @shippableUnits = $self->_getShippableUnits($cart);
my $packageCount = scalar @shippableUnits;
my $anyShippable = $packageCount > 0 ? 1 : 0;
return $cost unless $anyShippable;
#$cost = scalar @shippableUnits * $self->get('flatFee');
##Build XML ($cart, @shippableUnits)
foreach my $unit (@shippableUnits) {
if ($packageCount > 200) {
WebGUI::Error::InvalidParam->throw(error => q{Cannot do UPS lookups for more than 200 items.});
}
my $xml = $self->buildXML($cart, $unit);
##Do request ($xml)
my $response = $self->_doXmlRequest($xml);
##Error handling
if (! $response->is_success) {
WebGUI::Error::Shop::RemoteShippingRate->throw(error => 'Problem connecting to UPS Web Tools: '. $response->status_line);
}
my $returnedXML = $response->content;
my $xmlData = XMLin($returnedXML, ForceArray => [qw/RatedPackage/]);
if (! $xmlData->{Response}->{ResponseStatusCode}) {
WebGUI::Error::Shop::RemoteShippingRate->throw(error => 'Problem with UPS Online Tools XML: '. $xmlData->{Response}->{Error}->{ErrorDescription});
}
##Summarize costs from returned data
$cost += $self->_calculateFromXML($xmlData);
}
return $cost;
}
#-------------------------------------------------------------------
=head2 _calculateFromXML ( $xmlData )
Takes data from the UPS and returns the calculated shipping price.
=head3 $xmlData
Processed data from an XML rate request, as a perl data structure. The data is expected to
have this structure:
{
RatedShipment => {
TotalCharges => {
MonetaryValue => xx.yy
}
}
}
=cut
sub _calculateFromXML {
my ($self, $xmlData) = @_;
##Additional error checking on the XML data can be done in here. Or, in the future,
##individual elements of the cost can be parsed and returned.
return $xmlData->{RatedShipment}->{TotalCharges}->{MonetaryValue};
}
#-------------------------------------------------------------------
=head2 definition ( $session )
This subroutine returns an arrayref of hashrefs, used to validate data put into
the object by the user, and to automatically generate the edit form to show
the user.
=cut
sub definition {
my $class = shift;
my $session = shift;
WebGUI::Error::InvalidParam->throw(error => q{Must provide a session variable})
unless ref $session eq 'WebGUI::Session';
my $definition = shift || [];
my $i18n = WebGUI::International->new($session, 'ShipDriver_UPS');
tie my %shippingTypes, 'Tie::IxHash';
##Other shipping types can be added below, but also need to be handled by the
##javascript.
$shippingTypes{'us domestic'} = $i18n->get('us domestic');
$shippingTypes{'us international'} = $i18n->get('us international');
tie my %shippingServices, 'Tie::IxHash';
##Note, these keys are required XML keywords in the UPS XML API.
##It needs a one of every key, regardless of the correct label.
##The right set of options is set via JavaScript in the form.
$shippingServices{'01'} = $i18n->get('us domestic 01');
$shippingServices{'02'} = $i18n->get('us domestic 02');
$shippingServices{'03'} = $i18n->get('us domestic 03');
$shippingServices{'07'} = $i18n->get('us international 07');
$shippingServices{'08'} = $i18n->get('us international 08');
$shippingServices{'11'} = $i18n->get('us international 11');
$shippingServices{'12'} = $i18n->get('us domestic 12');
$shippingServices{'13'} = $i18n->get('us domestic 13');
$shippingServices{'14'} = $i18n->get('us domestic 14');
$shippingServices{'54'} = $i18n->get('us international 54');
$shippingServices{'59'} = $i18n->get('us domestic 59');
$shippingServices{'65'} = $i18n->get('us international 65');
tie my %pickupTypes, 'Tie::IxHash';
##Note, these keys are required XML keywords in the UPS XML API.
$pickupTypes{'01'} = $i18n->get('pickup code 01');
$pickupTypes{'03'} = $i18n->get('pickup code 03');
$pickupTypes{'06'} = $i18n->get('pickup code 06');
$pickupTypes{'07'} = $i18n->get('pickup code 07');
$pickupTypes{'11'} = $i18n->get('pickup code 11');
$pickupTypes{'19'} = $i18n->get('pickup code 19');
$pickupTypes{'20'} = $i18n->get('pickup code 20');
tie my %customerClassification, 'Tie::IxHash';
##Note, these keys are required XML keywords in the UPS XML API.
$customerClassification{'01'} = $i18n->get('customer classification 01');
$customerClassification{'03'} = $i18n->get('customer classification 03');
$customerClassification{'04'} = $i18n->get('customer classification 04');
my $localizedCountries = Locales::Country->new('en'); ##Note, for future i18n change the locale
tie my %localizedCountries, 'Tie::IxHash';
%localizedCountries = map { $_ => $_ } grep { !ref $_ } $localizedCountries->all_country_names();
tie my %fields, 'Tie::IxHash';
%fields = (
instructions => {
fieldType => 'readOnly',
label => $i18n->get('instructions'),
defaultValue => $i18n->get('ups instructions'),
noFormProcess => 1,
},
userId => {
fieldType => 'text',
label => $i18n->get('userid'),
hoverHelp => $i18n->get('userid help'),
defaultValue => '',
},
password => {
fieldType => 'password',
label => $i18n->get('password'),
hoverHelp => $i18n->get('password help'),
defaultValue => '',
},
licenseNo => {
fieldType => 'text',
label => $i18n->get('license'),
hoverHelp => $i18n->get('license help'),
defaultValue => '',
},
sourceZip => {
fieldType => 'zipcode',
label => $i18n->get('source zipcode'),
hoverHelp => $i18n->get('source zipcode help'),
defaultValue => '',
},
sourceCountry => {
fieldType => 'selectBox',
label => $i18n->get('source country'),
hoverHelp => $i18n->get('source country help'),
options => \%localizedCountries,
defaultValue => 'US',
},
shipType => {
fieldType => 'selectBox',
label => $i18n->get('ship type'),
hoverHelp => $i18n->get('ship type help'),
options => \%shippingTypes,
defaultValue => 'us domestic',
extras => q{onchange="WebGUI.ShipDriver.UPS.changeServices(this.options[this.selectedIndex].value,'shipService_formId')"},
},
shipService => {
fieldType => 'selectBox',
label => $i18n->get('ship service'),
hoverHelp => $i18n->get('ship service help'),
options => \%shippingServices,
defaultValue => '03',
},
pickupType => {
fieldType => 'selectBox',
label => $i18n->get('pickup type'),
hoverHelp => $i18n->get('pickup type help'),
options => \%pickupTypes,
defaultValue => '01',
},
customerClassification => {
fieldType => 'selectBox',
label => $i18n->get('customer classification'),
hoverHelp => $i18n->get('customer classification help'),
options => \%customerClassification,
defaultValue => '01',
},
residentialIndicator => {
fieldType => 'radioList',
label => $i18n->get('residential'),
hoverHelp => $i18n->get('residential help'),
options => {
residential => $i18n->get('residential'),
commercial => $i18n->get('commercial'),
},
defaultValue => 'commercial',
},
##Note, if a flat fee is added to this driver, then according to the license
##terms the website must display a note to the user (shop customer) that additional
##fees have been added.
# flatFee => {
# fieldType => 'float',
# label => $i18n->get('flatFee'),
# hoverHelp => $i18n->get('flatFee help'),
# defaultValue => 0,
# },
);
my %properties = (
name => 'UPS',
properties => \%fields,
);
push @{ $definition }, \%properties;
return $class->SUPER::definition($session, $definition);
}
#-------------------------------------------------------------------
=head2 _doXmlRequest ( $xml )
Contact the UPS website and submit the XML for a shipping rate lookup.
Returns a LWP::UserAgent response object.
=head3 $xml
XML to send. It has some very high standards, including XML components in
the right order and sets of allowed tags.
=cut
sub _doXmlRequest {
my ($self, $xml) = @_;
my $userAgent = LWP::UserAgent->new;
$userAgent->env_proxy;
$userAgent->agent('WebGUI');
#
my $url;
if ($self->testMode) {
$url = 'https://wwwcie.ups.com/ups.app/xml/Rate';
}
else {
$url = 'https://wwwcie.ups.com/ups.app/xml/Rate';
}
my $request = HTTP::Request->new(POST => $url);
$request->content_type( 'text/xml' );
$request->content( $xml );
my $response = $userAgent->request($request);
return $response;
}
#-------------------------------------------------------------------
=head2 getEditForm ( )
Override the master method to stuff in some javascript.
=cut
sub getEditForm {
my $self = shift;
$self->session->style->setScript(
$self->session->url->extras('yui/build/utilities/utilities.js'),
{ type => 'text/javascript', },
);
$self->session->style->setScript(
$self->session->url->extras('yui/build/yahoo-dom-event/yahoo-dom-event.js'),
{ type => 'text/javascript', },
);
$self->session->style->setScript(
$self->session->url->extras('yui/build/json/json-min.js'),
{ type => 'text/javascript', },
);
$self->session->style->setScript(
$self->session->url->extras('yui-webgui/build/i18n/i18n.js'),
{ type => 'text/javascript', },
);
$self->session->style->setScript(
$self->session->url->extras('yui-webgui/build/ShipDriver/UPS.js'),
{ type => 'text/javascript', },
);
$self->session->style->setRawHeadTags(<<EOL);
<script type="text/javascript">
YAHOO.util.Event.onDOMReady( WebGUI.ShipDriver.UPS.initI18n );
</script>
EOL
return $self->SUPER::getEditForm();
}
#-------------------------------------------------------------------
=head2 _getShippableUnits ( $cart )
This is a private method.
Sorts items into the cart by how they must be shipped; together, separate,
etc, following these rules:
=over 4
=item *
Each item which ships separately is 1 shippable unit.
=item *
All loose items are bundled together by zip code.
=back
This method returns a
For an empty cart (which shouldn't ever happen), it would return an empty array.
=head3 $cart
A WebGUI::Shop::Cart object. It provides access to the items in the cart
that must be sorted.
=cut
sub _getShippableUnits {
my ($self, $cart) = @_;
##All units sorted by zip code. Loose units kept separately so they
##can be easily bundled together by zip code.
my %shippableUnits = ();
my %looseUnits = ();
ITEM: foreach my $item (@{$cart->getItems}) {
my $sku = $item->getSku;
next ITEM unless $sku->isShippingRequired;
my $zip = $item->getShippingAddress->get('code');
if ($sku->shipsSeparately) {
push @{ $shippableUnits{$zip} }, ( [ $item ] ) x $item->get('quantity');
}
else {
push @{ $looseUnits{$zip} }, $item;
}
}
##Merge the two together now
while (my ($zip, $units) = each %looseUnits) {
push @{ $shippableUnits{$zip} }, $units;
}
return values %shippableUnits;
}
1;

View file

@ -0,0 +1,295 @@
package WebGUI::i18n::English::ShipDriver_UPS;
use strict;
our $I18N = {
'userid' => {
message => q|UPS UserId|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'userid help' => {
message => q|You can get a UserId by first registering with the UPS.|,
lastUpdated => 1203569511,
},
'password' => {
message => q|UPS Password|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'password help' => {
message => q|You will recieve a password along with your UserId when you register.|,
lastUpdated => 1203569511,
},
'license' => {
message => q|UPS Access License Number|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'license help' => {
message => q|You will recieve a license along with your UserId and password when you register.|,
lastUpdated => 1203569511,
},
'instructions' => {
message => q|Registration Instructions|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'ups instructions' => {
lastUpdated => 1241028258,
message => q|<p>In order to use the UPS Shipping Driver, you must first register with the UPS on their <a href="http://www.ups.com/e_comm_access/gettools_index?loc=en_US">website</a>. When you get to the step for an access key, be sure to get an XML access key. Enter your UPS username and password and access key into the form.</p><p>The driver currently supports domestic and international shipping from the United States. The weight property of a Product is considered to be in pounds. All currencies are in United States dollars. The package for shipping is a generic package, and there are no options for tubes, envelopes or fixed size packages available from the UPS.</p>|,
},
'ship service' => {
message => q|Shipping service|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'ship service help' => {
message => q|Select one from the list of options. If you wish to provide multiple types of shipping, create one additional shipping driver instance for each option.|,
lastUpdated => 1203569511,
},
'pickup type' => {
message => q|Pickup Type|,
lastUpdated => 1243006539,
context => q|Label in the ShipDriver edit form.|,
},
'pickup type help' => {
message => q|Select how the packages will be delivered to the UPS for shipping.|,
lastUpdated => 1203569511,
},
'source zipcode' => {
message => q|Shipping Zipcode|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'source zipcode help' => {
message => q|The zipcode of the location you will be shipping from.|,
lastUpdated => 1203569511,
},
'source country' => {
message => q|Shipping Country|,
lastUpdated => 1242945847,
context => q|Label in the ShipDriver edit form.|,
},
'source country help' => {
message => q|The country you will be shipping from.|,
lastUpdated => 1242945844,
},
'customer classification' => {
message => q|Customer Classification|,
lastUpdated => 1241214572,
context => q|What kind or type of customer are you?|,
},
'customer classification help' => {
message => q|The kind or type of customer you are.|,
lastUpdated => 1247110533,
},
'residentialIndicator' => {
message => q|Residential or Commercial?|,
lastUpdated => 1248113596,
context => q|Residential (a person's home) versus Commercial, a business address.|,
},
'residential help' => {
message => q|The UPS rates for delivering to a residential address, or a commercial address differ. WebGUI will not ask the user which is which, so you will need to configure drivers for both kinds of destinations.|,
lastUpdated => 1248113598,
},
'residential' => {
message => q|Residential|,
lastUpdated => 1248113596,
context => q|Residential (a person's home)|,
},
'commercial' => {
message => q|Commercial|,
lastUpdated => 1248113596,
context => q|A business address|,
},
'customer classification 01' => {
message => q|Wholesale|,
lastUpdated => 1247110533,
},
'customer classification 03' => {
message => q|Occasional|,
lastUpdated => 1247110533,
},
'customer classification 04' => {
message => q|Retail|,
lastUpdated => 1247110533,
},
'flatFee' => {
message => q|Flat Fee|,
lastUpdated => 1241214572,
context => q|A fixed amount of money added to a purchase for shipping.|,
},
'flatFee help' => {
message => q|A fixed amount of money added to a purchase for shipping, covering shipping materials and handling.|,
lastUpdated => 1241214575,
},
'us domestic 01' => {
message => q|UPS Next Day Air|,
lastUpdated => 1203569511,
context => q|Label for a type of shipping from the UPS.|,
},
'us domestic 02' => {
message => q|UPS Second Day Air|,
lastUpdated => 1203569511,
context => q|Label for a type of shipping from the UPS.|,
},
'us domestic 03' => {
message => q|UPS Ground|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'us domestic 12' => {
message => q|UPS Three-Day Select|,
lastUpdated => 1203569511,
context => q|Label for a type of shipping from the UPS.|,
},
'us domestic 13' => {
message => q|UPS Next Day Air Saver|,
lastUpdated => 1203569511,
context => q|Label for a type of shipping from the UPS.|,
},
'us domestic 14' => {
message => q|UPS Next Day Air Early A.M.|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'us domestic 59' => {
message => q|UPS Second Day Air A.M.|,
lastUpdated => 1203569511,
context => q|Label for a type of shipping from the UPS.|,
},
'pickup code 01' => {
message => q|Daily Pickup|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'pickup code 03' => {
message => q|Customer Counter|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'pickup code 06' => {
message => q|One Time Pickup|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'pickup code 07' => {
message => q|On Call Air|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'pickup code 11' => {
message => q|Suggested Retail Rates|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'pickup code 19' => {
message => q|Letter Center|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'pickup code 20' => {
message => q|Air Service Center|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'us domestic' => {
message => q|US Domestic|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'us international' => {
message => q|US International|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'ship type' => {
message => q|Shipping Type|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'ship type help' => {
message => q|Pick a type of shipping that will be used. The different types have different services available. Not all services are available in all types, or in all countries. Changing the service will change the Ship Service options below.|,
lastUpdated => 1247111015,
context => q|Label for a type of shipping from the UPS.|,
},
'us international 07' => {
message => q|UPS Worldwide Express|,
lastUpdated => 1203569511,
context => q|Label for a type of shipping from the UPS.|,
},
'us international 08' => {
message => q|UPS Worldwide Expedited|,
lastUpdated => 1203569511,
context => q|Label for a type of shipping from the UPS.|,
},
'us international 11' => {
message => q|UPS Standard|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'us international 54' => {
message => q|UPS Worldwide Express Plus|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
'us international 65' => {
message => q|UPS Saver|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the UPS.|,
},
};
1;