Add USPS Shipping Driver with tests, and i18n.

Change the Ship, Cart to do exception handling when calling a shipping driver,
as that's the message passing interface between the modules.
Add a noFormProcess field property that is recognized by processPropertiesFromFormPost.
This commit is contained in:
Colin Kuskie 2009-05-12 23:58:19 +00:00
parent 909c8f62ae
commit cbdd6077d5
10 changed files with 1209 additions and 18 deletions

View file

@ -21,6 +21,7 @@
transactions. (Martin Kamerbeek / Oqapi )
- Added better Survey Expression Engine validation warnings
- added #9203: Survey Visualization
- Added: United States Postal Service Shipping Driver.
7.7.5
- Adding StoryManager.

View file

@ -33,6 +33,7 @@ addTemplateAttachmentsTable($session);
revertUsePacked( $session );
fixDefaultPostReceived($session);
addEuVatDbColumns( $session );
addShippingDrivers( $session );
addTransactionTaxColumns( $session );
finish($session);
@ -91,6 +92,15 @@ EOSQL
print "DONE!\n" unless $quiet;
}
#----------------------------------------------------------------------------
# Describe what our function does
sub addShippingDrivers {
my $session = shift;
print "\tAdding columns for improved VAT number checking..." unless $quiet;
$session->config->addToArray('shippingDrivers', 'WebGUI::Shop::ShipDriver::USPS');
print "DONE!\n" unless $quiet;
}
#----------------------------------------------------------------------------
sub addEuVatDbColumns {
my $session = shift;

View file

@ -183,7 +183,8 @@
# available for configuration on the site.
"shippingDrivers" : [
"WebGUI::Shop::ShipDriver::FlatRate"
"WebGUI::Shop::ShipDriver::FlatRate",
"WebGUI::Shop::ShipDriver::USPS"
],
# Specify the list of template parsers available in the system.

View file

@ -20,7 +20,12 @@ use Exception::Class (
'WebGUI::Error::Shop::MaxOfItemInCartReached' => {
description => "Some items restrict how many you can put into your cart.",
},
},
'WebGUI::Error::Shop::RemoteShippingRate' => {
description => "Errors during the remote rate lookups.",
},
);

View file

@ -510,9 +510,14 @@ sub readyForCheckout {
return 0 if ($self->hasMixedItems);
# Check minimum cart checkout requirement
my $total = eval { $self->calculateTotal };
if (my $e = WebGUI::Error->caught) {
$error{id $self} = $e->error;
return 0;
}
my $requiredAmount = $self->session->setting->get( 'shopCartCheckoutMinimum' );
if ( $requiredAmount > 0 ) {
return 0 if $self->calculateTotal < $requiredAmount;
return 0 if $total < $requiredAmount;
}
# All checks passed so return true

View file

@ -97,12 +97,17 @@ sub getOptions {
WebGUI::Error::InvalidParam->throw(error => q{Need a cart.}) unless defined $cart and $cart->isa("WebGUI::Shop::Cart");
my $session = $cart->session;
my %options = ();
foreach my $shipper (@{$self->getShippers()}) {
next unless $shipper->get('enabled');
SHIPPER: foreach my $shipper (@{$self->getShippers()}) {
next SHIPPER unless $shipper->get('enabled');
my $price = eval { $shipper->calculate($cart) };
if (my $e = WebGUI::Error->caught()) {
$self->session->log->warn($e->error);
next SHIPPER;
}
$options{$shipper->getId} = {
label => $shipper->get("label"),
price => $shipper->calculate($cart),
};
price => $price,
};
}
return \%options;
}

View file

@ -32,8 +32,8 @@ These subroutines are available from this package:
=cut
readonly session => my %session;
private options => my %options;
private shipperId => my %shipperId;
private options => my %options;
private shipperId => my %shipperId;
#-------------------------------------------------------------------
@ -88,6 +88,9 @@ 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.
The optional hash key noFormProcess may be added to any field definition.
This will prevent that field from being processed by processPropertiesFromFormPost.
=cut
sub definition {
@ -261,17 +264,20 @@ Updates ship driver with data from Form.
=cut
sub processPropertiesFromFormPost {
my $self = shift;
my $self = shift;
my $session = $self->session;
my $form = $session->form;
my %properties;
my $fullDefinition = $self->definition($self->session);
my $fullDefinition = $self->definition($session);
foreach my $definition (@{$fullDefinition}) {
foreach my $property (keys %{$definition->{properties}}) {
$properties{$property} = $self->session->form->process(
$property,
$definition->{properties}{$property}{fieldType},
$definition->{properties}{$property}{defaultValue}
);
}
PROPERTY: foreach my $property (keys %{$definition->{properties}}) {
next PROPERTY if $definition->{properties}{$property}->{noFormProcess};
$properties{$property} = $form->process(
$property,
$definition->{properties}{$property}{fieldType},
$definition->{properties}{$property}{defaultValue}
);
}
}
$properties{title} = $fullDefinition->[0]{name} if ($properties{title} eq "" || lc($properties{title}) eq "untitled");
$self->update(\%properties);

View file

@ -0,0 +1,347 @@
package WebGUI::Shop::ShipDriver::USPS;
use strict;
use base qw/WebGUI::Shop::ShipDriver/;
use WebGUI::Exception;
use XML::Simple;
use LWP;
use Tie::IxHash;
=head1 NAME
Package WebGUI::Shop::ShipDriver::USPS
=head1 DESCRIPTION
Shipping driver for the United States Postal Service.
=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 US Postal Service servers
=head3 $cart
A WebGUI::Shop::Cart object. This allows us access to the user's
address book
=head3 @packages
An array of array references. Each array element is 1 set of items. The
quantity of items will vary in each set. If the quantity of an item
is more than 1, then we will check for shipping 1 item, and multiple the
result by the quantity, rather than doing several identical checks.
=cut
sub buildXML {
my ($self, $cart, @packages) = @_;
tie my %xmlHash, 'Tie::IxHash';
%xmlHash = ( RateV3Request => {}, );
my $xmlTop = $xmlHash{RateV3Request};
$xmlTop->{USERID} = $self->get('userId');
$xmlTop->{Package} = [];
##Do a request for each package.
my $packageIndex;
PACKAGE: for(my $packageIndex = 0; $packageIndex < scalar @packages; $packageIndex++) {
my $package = $packages[$packageIndex];
next PACKAGE unless scalar @{ $package };
tie my %packageData, 'Tie::IxHash';
my $weight = 0;
foreach my $item (@{ $package }) {
my $sku = $item->getSku;
my $itemWeight = $sku->getWeight();
##Items that ship separately with a quantity > 1 are rate estimated as 1 item and then the
##shipping cost is multiplied by the quantity.
if (! $sku->shipsSeparately ) {
$itemWeight *= $item->get('quantity');
}
$weight += $itemWeight;
}
my $pounds = int($weight);
my $ounces = int(16 * ($weight - $pounds));
my $destination = $package->[0]->getShippingAddress;
my $destZipCode = $destination->get('code');
$packageData{ID} = $packageIndex;
$packageData{Service} = [ $self->get('shipType') ];
$packageData{ZipOrigination} = [ $self->get('sourceZip') ];
$packageData{ZipDestination} = [ $destZipCode ];
$packageData{Pounds} = [ $pounds ];
$packageData{Ounces} = [ $ounces ];
if ($self->get('shipType') eq 'PRIORITY') {
$packageData{Container} = [ 'FLAT RATE BOX' ];
}
$packageData{Size} = [ 'REGULAR' ];
$packageData{Machinable} = [ 'true' ];
push @{ $xmlTop->{Package} }, \%packageData;
}
my $xml = XMLout(\%xmlHash,
KeepRoot => 1,
NoSort => 1,
NoIndent => 1,
KeyAttr => {
Package => 'ID',
},
SuppressEmpty => 0,
);
return $xml;
}
#-------------------------------------------------------------------
=head2 calculate ( $cart )
Returns a shipping price.
=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('userId')) {
WebGUI::Error::InvalidParam->throw(error => q{Driver configured without a USPS userId.});
}
my $cost = 0;
##Sort the items into shippable bundles.
my @shippableUnits = $self->_getShippableUnits($cart);
my $packageCount = scalar @shippableUnits;
if ($packageCount > 25) {
WebGUI::Error::InvalidParam->throw(error => q{Cannot do USPS lookups for more than 25 items.});
}
my $anyShippable = $packageCount > 0 ? 1 : 0;
return $cost unless $anyShippable;
#$cost = scalar @shippableUnits * $self->get('flatFee');
##Build XML ($cart, @shippableUnits)
my $xml = $self->buildXML($cart, @shippableUnits);
##Do request ($xml)
my $response = $self->_doXmlRequest($xml);
##Error handling
if (! $response->is_success) {
WebGUI::Error::RemoteShippingRate->throw(error => 'Problem connecting to USPS Web Tools: '. $response->status_line);
}
my $returnedXML = $response->content;
my $xmlData = XMLin($returnedXML, ForceArray => [qw/Package/]);
if (exists $xmlData->{Error}) {
WebGUI::Error::RemoteShippingRate->throw(error => 'Problem with USPS Web Tools XML: '. $xmlData->{Description});
}
##Summarize costs from returned data
$cost = $self->_calculateFromXML($xmlData, @shippableUnits);
return $cost;
}
#-------------------------------------------------------------------
=head2 _calculateFromXML ( $xmlData, @shippableUnits )
Takes data from the USPS and returns the calculated shipping price.
=head3 $xmlData
Processed XML data from an XML rate request, processed in perl data structure. The data is expected to
have this structure:
{
Package => [
{
ID => 0,
Postage => {
Rate => some_number
}
},
]
}
=head3 @shippableUnits
The set of shippable units, which are required to do quantity lookups.
=cut
sub _calculateFromXML {
my ($self, $xmlData, @shippableUnits) = @_;
my $cost = 0;
foreach my $package (@{ $xmlData->{Package} }) {
my $id = $package->{ID};
my $rate = $package->{Postage}->{Rate};
##Error check for invalid index
if ($id < 0 || $id > $#shippableUnits) {
WebGUI::Error::RemoteShippingRate->throw(error => "Illegal package index returned by USPS: $id");
}
my $unit = $shippableUnits[$id];
if ($unit->[0]->getSku->shipsSeparately) {
##This is a single item due to ships separately. Since in reality there will be
## N things being shipped, multiply the rate by the quantity.
$cost += $rate * $unit->[0]->get('quantity');
}
else {
##This is a loose bundle of items, all shipped together
$cost += $rate;
}
}
return $cost;
}
#-------------------------------------------------------------------
=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_USPS');
tie my %shippingTypes, 'Tie::IxHash';
##Note, these keys are required XML keywords in the USPS XML API.
$shippingTypes{'PRIORITY'} = $i18n->get('priority');
$shippingTypes{'EXPRESS' } = $i18n->get('express');
$shippingTypes{'PARCEL' } = $i18n->get('parcel post');
tie my %fields, 'Tie::IxHash';
%fields = (
instructions => {
fieldType => 'readOnly',
label => $i18n->get('instructions'),
defaultValue => $i18n->get('usps 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 => '',
},
sourceZip => {
fieldType => 'zipcode',
label => $i18n->get('source zipcode'),
hoverHelp => $i18n->get('source zipcode help'),
defaultValue => '',
},
shipType => {
fieldType => 'selectBox',
label => $i18n->get('ship type'),
hoverHelp => $i18n->get('ship type help'),
options => \%shippingTypes,
defaultValue => 'PARCEL',
},
##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 => 'U.S. Postal Service',
properties => \%fields,
);
push @{ $definition }, \%properties;
return $class->SUPER::definition($session, $definition);
}
#-------------------------------------------------------------------
=head2 _doXmlRequest ( $xml )
Contact the USPS 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 = 'http://production.shippingapis.com/ShippingAPI.dll?API=RateV3&XML=';
$url .= $xml;
my $request = HTTP::Request->new(GET => $url);
my $response = $userAgent->request($request);
return $response;
}
#-------------------------------------------------------------------
=head2 _getShippableUnits ( $cart )
This is a private method.
Sorts items into the cart by how they must be shipped, together, separate,
etc. Returns an array of array references of cart items grouped by
whether or not they ship separately, and then sorted by destination
zip code.
If an item in the cart must be shipped separately, but has a quantity greater
than 1, then for the purposes of looking up shipping costs it is returned
as 1 bundle, since the total cost can now be calculated by multiplying the
quantity together with the cost for a single unit.
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) = @_;
my @shippableUnits = ();
##Loose units are sorted by zip code.
my %looseUnits = ();
ITEM: foreach my $item (@{$cart->getItems}) {
my $sku = $item->getSku;
next ITEM unless $sku->isShippingRequired;
if ($sku->shipsSeparately) {
push @shippableUnits, [ $item ];
}
else {
my $zip = $item->getShippingAddress->get('code');
push @{ $looseUnits{$zip} }, $item;
}
}
push @shippableUnits, values %looseUnits;
return @shippableUnits;
}
1;

View file

@ -0,0 +1,93 @@
package WebGUI::i18n::English::ShipDriver_USPS;
use strict;
our $I18N = {
'userid' => {
message => q|USPS Web Tools Username|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'userid help' => {
message => q|You can get a Web Tools Username by first registering with the USPS.|,
lastUpdated => 1203569511,
},
'password' => {
message => q|USPS Web Tools Password (optional)|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'password help' => {
message => q|You will recieve a password along with your username when you register.|,
lastUpdated => 1203569511,
},
'instructions' => {
message => q|Registration Instructions|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'usps instructions' => {
lastUpdated => 1241028258,
message => q|<p>In order to use the USPS Shipping Driver, you must first register with the United States Postal Service as a <a href="https://secure.shippingapis.com/registration/">USPS Web Tools User</a>. Fill out the form, submit it, and within a few days the USPS will send you a username and password to use this service. Enter your username and password in the form fields below.</p><p>This driver supports three kinds of shipping with one preset size for each kind. Package sizes, and shipping services outside of those choices, are currently not supported.</p><p>For the purpose of calculating weight, the weight property of a Product is considered to be in pounds.|,
},
'ship type' => {
message => q|Shipping type|,
lastUpdated => 1203569535,
context => q|Label in the ShipDriver edit form.|,
},
'ship type 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,
},
'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,
},
'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,
},
'priority' => {
message => q|Priority, Flat Rate Box|,
lastUpdated => 1203569511,
context => q|Label for a type of shipping from the USPS.|,
},
'express' => {
message => q|Express, Regular size|,
lastUpdated => 1203569511,
context => q|Label for a type of shipping from the USPS.|,
},
'parcel post' => {
message => q|Parcel Post, Regular size|,
lastUpdated => 1242166045,
context => q|Label for a type of shipping from the USPS.|,
},
};
1;

718
t/Shop/ShipDriver/USPS.t Normal file
View file

@ -0,0 +1,718 @@
# vim:syntax=perl
#-------------------------------------------------------------------
# WebGUI is Copyright 2001-2009 Plain Black Corporation.
#-------------------------------------------------------------------
# Please read the legal notices (docs/legal.txt) and the license
# (docs/license.txt) that came with this distribution before using
# this software.
#------------------------------------------------------------------
# http://www.plainblack.com info@plainblack.com
#------------------------------------------------------------------
# Write a little about what this script tests.
#
#
use FindBin;
use strict;
use lib "$FindBin::Bin/../../lib";
use Test::More;
use Test::Deep;
use XML::Simple;
use Data::Dumper;
use WebGUI::Test; # Must use this before any other WebGUI modules
use WebGUI::Session;
#----------------------------------------------------------------------------
# Init
my $session = WebGUI::Test->session;
my $user = WebGUI::User->create($session);
WebGUI::Test->usersToDelete($user);
$session->user({user => $user});
#----------------------------------------------------------------------------
# Tests
my $tests = 41;
plan tests => 1 + $tests;
#----------------------------------------------------------------------------
# put your tests here
my $loaded = use_ok('WebGUI::Shop::ShipDriver::USPS');
my $storage;
my ($driver, $cart);
my $versionTag = WebGUI::VersionTag->getWorking($session);
my $home = WebGUI::Asset->getDefault($session);
my $rockHammer = $home->addChild({
className => 'WebGUI::Asset::Sku::Product',
isShippingRequired => 1, title => 'Rock Hammers',
shipsSeparately => 0,
});
my $smallHammer = $rockHammer->setCollateral('variantsJSON', 'variantId', 'new',
{
shortdesc => 'Small rock hammer', price => 7.50,
varSku => 'small-hammer', weight => 1.5,
quantity => 9999,
}
);
my $bigHammer = $rockHammer->setCollateral('variantsJSON', 'variantId', 'new',
{
shortdesc => 'Big rock hammer', price => 19.99,
varSku => 'big-hammer', weight => 12,
quantity => 9999,
}
);
my $bible = $home->addChild({
className => 'WebGUI::Asset::Sku::Product',
isShippingRequired => 1, title => 'Bibles, individuall wrapped and shipped',
shipsSeparately => 1,
});
my $kjvBible = $bible->setCollateral('variantsJSON', 'variantId', 'new',
{
shortdesc => 'King James Bible', price => 17.50,
varSku => 'kjv-bible', weight => 2.5,
quantity => 99999,
}
);
my $nivBible = $bible->setCollateral('variantsJSON', 'variantId', 'new',
{
shortdesc => 'NIV Bible', price => 22.50,
varSku => 'niv-bible', weight => 2.0,
quantity => 999999,
}
);
$versionTag->commit;
SKIP: {
skip 'Unable to load module WebGUI::Shop::ShipDriver::USPS', $tests unless $loaded;
#######################################################################
#
# definition
#
#######################################################################
my $definition;
my $e; ##Exception variable, used throughout the file
eval { $definition = WebGUI::Shop::ShipDriver::USPS->definition(); };
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'definition takes an exception to not giving it a session variable');
cmp_deeply(
$e,
methods(
error => 'Must provide a session variable',
),
'... checking error message',
);
isa_ok(
$definition = WebGUI::Shop::ShipDriver::USPS->definition($session),
'ARRAY'
);
#######################################################################
#
# create
#
#######################################################################
my $options = {
label => 'USPS Driver',
enabled => 1,
};
$driver = WebGUI::Shop::ShipDriver::USPS->create($session, $options);
isa_ok($driver, 'WebGUI::Shop::ShipDriver::USPS');
isa_ok($driver, 'WebGUI::Shop::ShipDriver');
#######################################################################
#
# getName
#
#######################################################################
is (WebGUI::Shop::ShipDriver::USPS->getName($session), 'U.S. Postal Service', 'getName returns the human readable name of this driver');
#######################################################################
#
# delete
#
#######################################################################
my $driverId = $driver->getId;
$driver->delete;
my $count = $session->db->quickScalar('select count(*) from shipper where shipperId=?',[$driverId]);
is($count, 0, 'delete deleted the object');
undef $driver;
#######################################################################
#
# calculate, and private methods.
#
#######################################################################
$driver = WebGUI::Shop::ShipDriver::USPS->create($session, {
label => 'Shipping from Shawshank',
enabled => 1,
shipType => 'PARCEL',
});
eval { $driver->calculate() };
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'calculate throws an exception when no zipcode has been set');
cmp_deeply(
$e,
methods(
error => 'Driver configured without a source zipcode.',
),
'... checking error message',
);
my $properties = $driver->get();
$properties->{sourceZip} = '97123';
$driver->update($properties);
eval { $driver->calculate() };
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'calculate throws an exception when no userId');
cmp_deeply(
$e,
methods(
error => 'Driver configured without a USPS userId.',
),
'... checking error message',
);
$cart = WebGUI::Shop::Cart->newBySession($session);
my $addressBook = $cart->getAddressBook;
my $workAddress = $addressBook->addAddress({
label => 'work',
organization => 'Plain Black Corporation',
address1 => '1360 Regent St. #145',
city => 'Madison', state => 'WI', code => '53715',
country => 'USA',
});
my $wucAddress = $addressBook->addAddress({
label => 'wuc',
organization => 'Madison Concourse Hotel',
address1 => '1 W Dayton St',
city => 'Madison', state => 'WI', code => '53703',
country => 'USA',
});
$cart->update({shippingAddressId => $workAddress->getId});
cmp_deeply(
[$driver->_getShippableUnits($cart)],
[(), ],
'_getShippableUnits: empty cart'
);
$rockHammer->addToCart($rockHammer->getCollateral('variantsJSON', 'variantId', $smallHammer));
cmp_deeply(
[$driver->_getShippableUnits($cart)],
[[ ignore() ], ],
'_getShippableUnits: one loose item in the cart'
);
$rockHammer->addToCart($rockHammer->getCollateral('variantsJSON', 'variantId', $bigHammer));
cmp_deeply(
[$driver->_getShippableUnits($cart)],
[[ ignore(), ignore() ], ],
'_getShippableUnits: two loose items in the cart'
);
$bible->addToCart($bible->getCollateral('variantsJSON', 'variantId', $kjvBible));
cmp_bag(
[$driver->_getShippableUnits($cart)],
[[ ignore(), ignore() ], [ ignore(), ], ],
'_getShippableUnits: two loose items, and 1 ships separately item in the cart'
);
my $bibleItem = $bible->addToCart($bible->getCollateral('variantsJSON', 'variantId', $nivBible));
$bibleItem->setQuantity(5);
cmp_bag(
[$driver->_getShippableUnits($cart)],
[[ ignore(), ignore() ], [ ignore() ], [ ignore() ], ],
'_getShippableUnits: two loose items, and 2 ships separately item in the cart, regarless of quantity for the new item'
);
my $rockHammer2 = $bible->addToCart($rockHammer->getCollateral('variantsJSON', 'variantId', $smallHammer));
$rockHammer2->update({shippingAddressId => $wucAddress->getId});
cmp_bag(
[$driver->_getShippableUnits($cart)],
[[ ignore(), ignore() ], [ ignore() ], [ ignore() ], [ ignore() ], ],
'_getShippableUnits: two loose items, and 2 ships separately item in the cart, and another loose item sorted by zipcode'
);
$cart->empty;
$bible->addToCart($bible->getCollateral('variantsJSON', 'variantId', $nivBible));
cmp_deeply(
[$driver->_getShippableUnits($cart)],
[ [ ignore() ], ],
'_getShippableUnits: only 1 ships separately item in the cart'
);
$cart->empty;
my $userId = $session->config->get('testing/USPS_userId');
my $hasRealUserId = 1;
##If there isn't a userId, set a fake one for XML testing.
if (! $userId) {
$hasRealUserId = 0;
$userId = "blahBlahBlah";
}
$properties = $driver->get();
$properties->{userId} = $userId;
$properties->{sourceZip} = '97123';
$driver->update($properties);
$rockHammer->addToCart($rockHammer->getCollateral('variantsJSON', 'variantId', $smallHammer));
my @shippableUnits = $driver->_getShippableUnits($cart);
my $xml = $driver->buildXML($cart, @shippableUnits);
like($xml, qr/<RateV3Request USERID="[^"]+"/, 'buildXML: checking userId is an attribute of the RateV3Request tag');
like($xml, qr/<Package ID="0"/, 'buildXML: checking ID is an attribute of the Package tag');
my $xmlData = XMLin($xml,
KeepRoot => 1,
ForceArray => ['Package'],
);
cmp_deeply(
$xmlData,
{
RateV3Request => {
USERID => $userId,
Package => [
{
ID => 0,
ZipDestination => '53715', ZipOrigination => '97123',
Pounds => '1', Ounces => '8',
Size => 'REGULAR', Service => 'PARCEL',
Machinable => 'true',
},
],
}
},
'buildXML: PARCEL service, 1 item in cart'
);
like($xml, qr/RateV3Request USERID.+?Package ID=.+?Service.+?ZipOrigination.+?ZipDestination.+?Pounds.+?Ounces.+?Size.+?Machinable/, '... and tag order');
SKIP: {
skip 'No userId for testing', 2 unless $hasRealUserId;
my $response = $driver->_doXmlRequest($xml);
ok($response->is_success, '_doXmlRequest to USPS successful');
my $xmlData = XMLin($response->content, ForceArray => [qw/Package/],);
cmp_deeply(
$xmlData,
{
Package => [
{
ID => 0,
ZipOrigination => ignore(), ZipDestination => ignore(),
Machinable => ignore(), Ounces => ignore(),
Pounds => ignore(), Size => ignore(),
Zone => ignore(),
Postage => {
CLASSID => ignore(),
MailService => ignore(),
Rate => num(10,10), ##A number around 10...
}
},
],
},
'... returned data from USPS in correct format. If this test fails, the driver may need to be updated'
);
}
my $cost = $driver->_calculateFromXML({
Package => [
{
ID => 0,
Postage => {
Rate => 5.25,
},
},
],
},
@shippableUnits
);
is($cost, 5.25, '_calculateFromXML calculates shipping cost correctly for 1 item in the cart');
$bibleItem = $bible->addToCart($bible->getCollateral('variantsJSON', 'variantId', $nivBible));
@shippableUnits = $driver->_getShippableUnits($cart);
$xml = $driver->buildXML($cart, @shippableUnits);
$xmlData = XMLin( $xml,
KeepRoot => 1,
ForceArray => ['Package'],
);
cmp_deeply(
$xmlData,
{
RateV3Request => {
USERID => $userId,
Package => [
{
ID => 0,
ZipDestination => '53715', ZipOrigination => '97123',
Pounds => '2', Ounces => '0',
Size => 'REGULAR', Service => 'PARCEL',
Machinable => 'true',
},
{
ID => 1,
ZipDestination => '53715', ZipOrigination => '97123',
Pounds => '1', Ounces => '8',
Size => 'REGULAR', Service => 'PARCEL',
Machinable => 'true',
},
],
}
},
'Validate XML structure and content for 2 items in the cart'
);
SKIP: {
skip 'No userId for testing', 2 unless $hasRealUserId;
my $response = $driver->_doXmlRequest($xml);
ok($response->is_success, '_doXmlRequest to USPS successful for 2 items in cart');
my $xmlData = XMLin($response->content, ForceArray => [qw/Package/],);
cmp_deeply(
$xmlData,
{
Package => [
{
ID => 0,
ZipOrigination => ignore(), ZipDestination => ignore(),
Machinable => ignore(), Ounces => 0,
Pounds => 2, Size => ignore(),
Zone => ignore(),
Postage => {
CLASSID => ignore(),
MailService => ignore(),
Rate => num(10,10), ##A number around 10...
}
},
{
ID => 1,
ZipOrigination => ignore(), ZipDestination => ignore(),
Machinable => ignore(), Ounces => 8,
Pounds => 1, Size => ignore(),
Zone => ignore(),
Postage => {
CLASSID => ignore(),
MailService => ignore(),
Rate => num(10,10), ##A number around 10...
}
},
],
},
'... returned data from USPS in correct format for 2 items in cart. If this test fails, the driver may need to be updated'
);
}
$cost = $driver->_calculateFromXML({
Package => [
{
ID => 0,
Postage => {
Rate => 7.00,
},
},
{
ID => 1,
Postage => {
Rate => 5.25,
},
},
],
},
@shippableUnits
);
is($cost, 12.25, '_calculateFromXML calculates shipping cost correctly for 2 items in the cart');
$bibleItem->setQuantity(2);
@shippableUnits = $driver->_getShippableUnits($cart);
$cost = $driver->_calculateFromXML({
Package => [
{
ID => 0,
Postage => {
Rate => 7.00,
},
},
{
ID => 1,
Postage => {
Rate => 5.25,
},
},
],
},
@shippableUnits
);
is($cost, 19.25, '_calculateFromXML calculates shipping cost correctly for 2 items in the cart, with quantity of 2');
$rockHammer2 = $rockHammer->addToCart($rockHammer->getCollateral('variantsJSON', 'variantId', $bigHammer));
$rockHammer2->update({shippingAddressId => $wucAddress->getId});
@shippableUnits = $driver->_getShippableUnits($cart);
$xml = $driver->buildXML($cart, @shippableUnits);
$xmlData = XMLin( $xml,
KeepRoot => 1,
ForceArray => ['Package'],
);
cmp_deeply(
$xmlData,
{
RateV3Request => {
USERID => $userId,
Package => [
{
ID => 0,
ZipDestination => '53715', ZipOrigination => '97123',
Pounds => '2', Ounces => '0',
Size => 'REGULAR', Service => 'PARCEL',
Machinable => 'true',
},
{
ID => 1,
ZipDestination => '53715', ZipOrigination => '97123',
Pounds => '1', Ounces => '8',
Size => 'REGULAR', Service => 'PARCEL',
Machinable => 'true',
},
{
ID => 2,
ZipDestination => '53703', ZipOrigination => '97123',
Pounds => '12', Ounces => '0',
Size => 'REGULAR', Service => 'PARCEL',
Machinable => 'true',
},
],
}
},
'Validate XML structure and content for 3 items in the cart, 3 shippable items'
);
SKIP: {
skip 'No userId for testing', 2 unless $hasRealUserId;
my $response = $driver->_doXmlRequest($xml);
ok($response->is_success, '_doXmlRequest to USPS successful for 3 items in cart');
my $xmlData = XMLin($response->content, ForceArray => [qw/Package/],);
cmp_deeply(
$xmlData,
{
Package => [
{
ID => 0,
ZipOrigination => ignore(), ZipDestination => ignore(),
Machinable => ignore(), Ounces => 0,
Pounds => 2, Size => ignore(),
Zone => ignore(),
Postage => {
CLASSID => ignore(),
MailService => ignore(),
Rate => num(10,10), ##A number around 10...
}
},
{
ID => 1,
ZipOrigination => ignore(), ZipDestination => ignore(),
Machinable => ignore(), Ounces => 8,
Pounds => 1, Size => ignore(),
Zone => ignore(),
Postage => {
CLASSID => ignore(),
MailService => ignore(),
Rate => num(10,10), ##A number around 10...
}
},
{
ID => 2,
ZipOrigination => ignore(), ZipDestination => 53703,
Machinable => ignore(), Ounces => 0,
Pounds => 12, Size => ignore(),
Zone => ignore(),
Postage => {
CLASSID => ignore(),
MailService => ignore(),
Rate => num(20,20), ##A number around 20...
}
},
],
},
'... returned data from USPS in correct format for 3 items in cart. If this test fails, the driver may need to be updated'
);
}
$cart->empty;
$properties = $driver->get();
$properties->{shipType} = 'PRIORITY';
$driver->update($properties);
$rockHammer->addToCart($rockHammer->getCollateral('variantsJSON', 'variantId', $smallHammer));
@shippableUnits = $driver->_getShippableUnits($cart);
$xml = $driver->buildXML($cart, @shippableUnits);
my $xmlData = XMLin($xml,
KeepRoot => 1,
ForceArray => ['Package'],
);
cmp_deeply(
$xmlData,
{
RateV3Request => {
USERID => $userId,
Package => [
{
ID => 0,
ZipDestination => '53715', ZipOrigination => '97123',
Pounds => '1', Ounces => '8',
Size => 'REGULAR', Service => 'PRIORITY',
Machinable => 'true', Container => 'FLAT RATE BOX',
},
],
}
},
'buildXML: PRIORITY service, 1 item in cart'
);
like($xml, qr/RateV3Request USERID.+?Package ID=.+?Service.+?ZipOrigination.+?ZipDestination.+?Pounds.+?Ounces.+?Container.+?Size.+?Machinable/, '... and tag order');
SKIP: {
skip 'No userId for testing', 2 unless $hasRealUserId;
my $response = $driver->_doXmlRequest($xml);
ok($response->is_success, '_doXmlRequest to USPS successful');
my $xmlData = XMLin($response->content, ForceArray => [qw/Package/],);
cmp_deeply(
$xmlData,
{
Package => [
{
ID => 0,
ZipOrigination => ignore(), ZipDestination => ignore(),
Container => ignore(), Ounces => ignore(), ##Machinable missing, added Container
Pounds => ignore(), Size => ignore(),
Zone => ignore(),
Postage => {
CLASSID => ignore(),
MailService => ignore(),
Rate => num(10,10), ##A number around 10...
}
},
],
},
'... returned data from USPS in correct format. If this test fails, the driver may need to be updated'
);
}
$properties = $driver->get();
$properties->{shipType} = 'EXPRESS';
$driver->update($properties);
$xml = $driver->buildXML($cart, @shippableUnits);
my $xmlData = XMLin($xml,
KeepRoot => 1,
ForceArray => ['Package'],
);
cmp_deeply(
$xmlData,
{
RateV3Request => {
USERID => $userId,
Package => [
{
ID => 0,
ZipDestination => '53715', ZipOrigination => '97123',
Pounds => '1', Ounces => '8',
Size => 'REGULAR', Service => 'EXPRESS',
Machinable => 'true',
},
],
}
},
'buildXML: EXPRESS service, 1 item in cart'
);
like($xml, qr/RateV3Request USERID.+?Package ID=.+?Service.+?ZipOrigination.+?ZipDestination.+?Pounds.+?Ounces.+?Size.+?Machinable/, '... and tag order');
SKIP: {
skip 'No userId for testing', 2 unless $hasRealUserId;
my $response = $driver->_doXmlRequest($xml);
ok($response->is_success, '... _doXmlRequest to USPS successful');
my $xmlData = XMLin($response->content, ForceArray => [qw/Package/],);
cmp_deeply(
$xmlData,
{
Package => [
{
ID => 0,
ZipOrigination => ignore(), ZipDestination => ignore(),
Ounces => ignore(), Pounds => ignore(),
Size => ignore(), Zone => ignore(),
Postage => {
CLASSID => ignore(),
MailService => ignore(),
Rate => num(30,30), ##A number around 10...
}
},
],
},
'... returned data from USPS in correct format. If this test fails, the driver may need to be updated'
);
}
}
#----------------------------------------------------------------------------
# Cleanup
END {
if (defined $driver && $driver->isa('WebGUI::Shop::ShipDriver')) {
$driver->delete;
}
if (defined $cart && $cart->isa('WebGUI::Shop::Cart')) {
my $addressBook = $cart->getAddressBook();
$addressBook->delete if $addressBook;
$cart->delete;
}
if (defined $versionTag) {
$versionTag->rollback;
}
}