diff --git a/lib/WebGUI/Shop/ShipDriver/USPS.pm b/lib/WebGUI/Shop/ShipDriver/USPS.pm index 23e5faba3..50a2f3fc9 100644 --- a/lib/WebGUI/Shop/ShipDriver/USPS.pm +++ b/lib/WebGUI/Shop/ShipDriver/USPS.pm @@ -151,6 +151,7 @@ sub calculate { } ##Summarize costs from returned data $cost = $self->_calculateFromXML($xmlData, @shippableUnits); + $cost += $self->_calculateInsurance(@shippableUnits); return $cost; } @@ -208,6 +209,74 @@ sub _calculateFromXML { #------------------------------------------------------------------- +=head2 _calculateInsurance ( @shippableUnits ) + +Takes data from the USPS and returns the calculated shipping price. + +=head3 @shippableUnits + +The set of shippable units, which are required to do quantity and cost lookups. + +=cut + +sub _calculateInsurance { + my ($self, @shippableUnits) = @_; + my $insuranceCost = 0; + return $insuranceCost unless $self->get('addInsurance') && $self->get('insuranceRates'); + my @insuranceTable = _parseInsuranceRates($self->get('insuranceRates')); + ##Sort by decreasing value for easy post processing + @insuranceTable = sort { $a->[0] <=> $b->[0] } @insuranceTable; + foreach my $package (@shippableUnits) { + my $value = 0; + ITEM: foreach my $item (@{ $package }) { + $value += $item->getSku->getPrice() * $item->get('quantity'); + } + my $pricePoint; + POINT: foreach my $point (@insuranceTable) { + if ($value < $point->[0]) { + $pricePoint = $point; + last POINT; + } + } + if (!defined $pricePoint) { + $pricePoint = $insuranceTable[-1]; + } + $insuranceCost += $pricePoint->[1]; + } + return $insuranceCost; +} + +#------------------------------------------------------------------- + +=head2 _parseInsuranceRates ( $rates ) + +Take the user entered data, a string, and turn it into an array. + +=head3 $rates + +The rate data entered by the user. One set of data per line. Each line has the value of +shipment, a colon, and the cost of insuring a shipment of that value. + +=cut + +sub _parseInsuranceRates { + my $rates = shift; + $rates =~ tr/\r//d; + my $number = qr/\d+(?:\.\d+)?/; + my $rate = qr{ \s* $number \s* : \s* $number \s* }x; + return () if ($rates !~ m{ \A (?: $rate \r?\n )* $rate (?:\r\n)? \Z }x); + my @lines = split /\n/, $rates; + my @table = (); + foreach my $line (@lines) { + $line =~ s/\s+//g; + my ($value, $cost) = split /:/, $line; + push @table, [ $value, $cost ]; + } + return @table; +} + +#------------------------------------------------------------------- + =head2 definition ( $session ) This subroutine returns an arrayref of hashrefs, used to validate data put into @@ -262,6 +331,18 @@ sub definition { options => \%shippingTypes, defaultValue => 'PARCEL', }, + addInsurance => { + fieldType => 'yesNo', + label => $i18n->get('add insurance'), + hoverHelp => $i18n->get('add insurance help'), + defaultValue => 0, + }, + insuranceRates => { + fieldType => 'textarea', + label => $i18n->get('insurance rates'), + hoverHelp => $i18n->get('insurance rates help'), + defaultValue => "50:1.75\n100:2.25", + }, ##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. diff --git a/lib/WebGUI/i18n/English/ShipDriver_USPS.pm b/lib/WebGUI/i18n/English/ShipDriver_USPS.pm index 3b9392784..036ee49d2 100644 --- a/lib/WebGUI/i18n/English/ShipDriver_USPS.pm +++ b/lib/WebGUI/i18n/English/ShipDriver_USPS.pm @@ -94,6 +94,30 @@ our $I18N = { context => q|Label for a type of shipping from the USPS.|, }, + 'add insurance' => { + message => q|Ship with insurance?|, + lastUpdated => 1253988886, + context => q|Label for the edit screen.|, + }, + + 'add insurance help' => { + message => q|If set to yes, the shipping plugin will ask the USPS for the cost of insuring this shipment. The cost will be added to the total cost of shipping. If insurance is not available, then the option to use this driver will not be presented to the user.|, + lastUpdated => 1253988884, + context => q|Label for a type of shipping from the USPS.|, + }, + + 'insurance rates' => { + message => q|Insurance Rate Table|, + lastUpdated => 1253988886, + context => q|Label for the edit screen.|, + }, + + 'insurance rates help' => { + message => q|Enter in one field per line with the format, value:cost.
value is the value of the contents.
cost is the cost of insurance at that value.
value and cost should look like numbers with a decimal point, like 0.50 or 1.00|, + lastUpdated => 1253988884, + context => q|Help for the insurance rate field.|, + }, + }; 1; diff --git a/t/Shop/ShipDriver/USPS.t b/t/Shop/ShipDriver/USPS.t index fa63039dc..9a73f28dc 100644 --- a/t/Shop/ShipDriver/USPS.t +++ b/t/Shop/ShipDriver/USPS.t @@ -24,7 +24,7 @@ use Data::Dumper; use WebGUI::Test; # Must use this before any other WebGUI modules use WebGUI::Session; -plan tests => 46; +plan tests => 64; use_ok('WebGUI::Shop::ShipDriver::USPS') or die 'Unable to load module WebGUI::Shop::ShipDriver::USPS'; @@ -42,8 +42,16 @@ $session->user({user => $user}); # put your tests here -my $storage; -my ($driver, $cart); +my ($driver2, $cart); +my $insuranceTable = <getWorking($session); my $home = WebGUI::Asset->getDefault($session); @@ -92,8 +100,16 @@ my $nivBible = $bible->setCollateral('variantsJSON', 'variantId', 'new', } ); +my $gospels = $bible->setCollateral('variantsJSON', 'variantId', 'new', + { + shortdesc => 'Gospels from the new Testament', + price => 1.50, varSku => 'gospels', + weight => 2.0, quantity => 999999, + } +); + $versionTag->commit; -WebGUI::Test->tagsToRollback($versionTag); +addToCleanup($versionTag); ####################################################################### # @@ -133,10 +149,11 @@ my $options = { enabled => 1, }; -$driver = WebGUI::Shop::ShipDriver::USPS->create($session, $options); +$driver2 = WebGUI::Shop::ShipDriver::USPS->create($session, $options); +addToCleanup($driver2); -isa_ok($driver, 'WebGUI::Shop::ShipDriver::USPS'); -isa_ok($driver, 'WebGUI::Shop::ShipDriver'); +isa_ok($driver2, 'WebGUI::Shop::ShipDriver::USPS'); +isa_ok($driver2, 'WebGUI::Shop::ShipDriver'); ####################################################################### # @@ -152,13 +169,13 @@ is (WebGUI::Shop::ShipDriver::USPS->getName($session), 'U.S. Postal Service', 'g # ####################################################################### -my $driverId = $driver->getId; -$driver->delete; +my $driverId = $driver2->getId; +$driver2->delete; my $count = $session->db->quickScalar('select count(*) from shipper where shipperId=?',[$driverId]); is($count, 0, 'delete deleted the object'); -undef $driver; +undef $driver2; ####################################################################### # @@ -166,11 +183,12 @@ undef $driver; # ####################################################################### -$driver = WebGUI::Shop::ShipDriver::USPS->create($session, { +my $driver = WebGUI::Shop::ShipDriver::USPS->create($session, { label => 'Shipping from Shawshank', enabled => 1, shipType => 'PARCEL', }); +addToCleanup($driver); eval { $driver->calculate() }; $e = Exception::Class->caught(); @@ -199,6 +217,7 @@ cmp_deeply( ); $cart = WebGUI::Shop::Cart->newBySession($session); +addToCleanup($cart); my $addressBook = $cart->getAddressBook; my $workAddress = $addressBook->addAddress({ label => 'work', @@ -283,6 +302,22 @@ $driver->update($properties); $rockHammer->addToCart($rockHammer->getCollateral('variantsJSON', 'variantId', $smallHammer)); my @shippableUnits = $driver->_getShippableUnits($cart); +$properties = $driver->get(); +$properties->{addInsurance} = 1; +$properties->{insuranceRates} = $insuranceTable; +$driver->update($properties); + +is($driver->_calculateInsurance(@shippableUnits), 2, '_calculateInsurance: one item in cart with quantity=1, calculates insurance'); + +$properties->{addInsurance} = 0; +$driver->update($properties); +is($driver->_calculateInsurance(@shippableUnits), 0, '_calculateInsurance: returns 0 if insurance is not enabled'); + +$properties->{addInsurance} = 1; +$properties->{insuranceRates} = ''; +$driver->update($properties); +is($driver->_calculateInsurance(@shippableUnits), 0, '_calculateInsurance: returns 0 if rates are not set'); + my $xml = $driver->buildXML($cart, @shippableUnits); like($xml, qr/addToCart($bible->getCollateral('variantsJSON', 'variantId', $nivBible)); @shippableUnits = $driver->_getShippableUnits($cart); -$xml = $driver->buildXML($cart, @shippableUnits); +is(calculateInsurance($driver), 7, '_calculateInsurance: two items in cart with quantity=1, calculates insurance'); + +$xml = $driver->buildXML($cart, @shippableUnits); $xmlData = XMLin( $xml, KeepRoot => 1, ForceArray => ['Package'], @@ -458,6 +495,8 @@ is($cost, 12.25, '_calculateFromXML calculates shipping cost correctly for 2 ite $bibleItem->setQuantity(2); @shippableUnits = $driver->_getShippableUnits($cart); +is(calculateInsurance($driver), 8, '_calculateInsurance: two items in cart with quantity=2, calculates insurance'); + $cost = $driver->_calculateFromXML({ Package => [ { @@ -481,6 +520,7 @@ is($cost, 19.25, '_calculateFromXML calculates shipping cost correctly for 2 ite $rockHammer2 = $rockHammer->addToCart($rockHammer->getCollateral('variantsJSON', 'variantId', $bigHammer)); $rockHammer2->update({shippingAddressId => $wucAddress->getId}); @shippableUnits = $driver->_getShippableUnits($cart); +is(calculateInsurance($driver), 12, '_calculateInsurance: calculates insurance'); $xml = $driver->buildXML($cart, @shippableUnits); $xmlData = XMLin( $xml, @@ -575,6 +615,12 @@ SKIP: { } +####################################################################### +# +# Test Priority shipping setup +# +####################################################################### + $cart->empty; $properties = $driver->get(); $properties->{shipType} = 'PRIORITY'; @@ -637,6 +683,12 @@ SKIP: { } +####################################################################### +# +# Test EXPRESS shipping setup +# +####################################################################### + $properties = $driver->get(); $properties->{shipType} = 'EXPRESS'; $driver->update($properties); @@ -695,6 +747,11 @@ SKIP: { } +####################################################################### +# +# Test PRIORITY VARIABLE shipping setup +# +####################################################################### $properties = $driver->get(); $properties->{shipType} = 'PRIORITY VARIABLE'; @@ -754,16 +811,60 @@ SKIP: { } +####################################################################### +# +# _calculateInsurance edge case +# +####################################################################### +$cart->empty; +$bible->addToCart($bible->getCollateral('variantsJSON', 'variantId', $gospels)); +@shippableUnits = $driver->_getShippableUnits($cart); +is(calculateInsurance($driver), 1, '_calculateInsurance: calculates insurance using the first bin'); + +####################################################################### +# +# _parseInsuranceRates +# +####################################################################### + +my @rates; +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates(""); +cmp_deeply(\@rates, [], '_parseInsuranceRates: empty string returns empty array'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates(); +cmp_deeply(\@rates, [], '_parseInsuranceRates: undef returns empty array'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates("2"); +cmp_deeply(\@rates, [], '... bad rates #1'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates(":2"); +cmp_deeply(\@rates, [], '... bad rates #2'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates("a:b"); +cmp_deeply(\@rates, [], '... bad rates #3'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates("2:2"); +cmp_deeply(\@rates, [ ['2', '2'] ], '... one line of good rates'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates("2.0:2.0"); +cmp_deeply(\@rates, [ ['2.0', '2.0'] ], '... one line of good rates with decimal points'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates("2.0:2.0\n"); +cmp_deeply(\@rates, [ ['2.0', '2.0'] ], '... one line of good rates with newline'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates("2.0:2.0\r\n"); +cmp_deeply(\@rates, [ ['2.0', '2.0'] ], '... one line of good rates with cr/newline'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates("2.0 : 2.0\r\n"); +cmp_deeply(\@rates, [ ['2.0', '2.0'] ], '... one line of good rates with cr/newline and spaces'); +@rates = WebGUI::Shop::ShipDriver::USPS::_parseInsuranceRates(" 2.0 : 2.0 \r\n"); +cmp_deeply(\@rates, [ ['2.0', '2.0'] ], '... one line of good rates with cr/newline and more spaces'); #---------------------------------------------------------------------------- # 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; - } + +sub calculateInsurance { + my $driver = shift; + my $properties = $driver->get(); + $properties->{addInsurance} = 1; + $properties->{insuranceRates} = $insuranceTable; + $driver->update($properties); + + my $insurance = $driver->_calculateInsurance(@shippableUnits); + + $properties->{addInsurance} = 0; + $driver->update($properties); + + return $insurance; } diff --git a/t/lib/WebGUI/Test.pm b/t/lib/WebGUI/Test.pm index 649f73356..2a0472bb1 100644 --- a/t/lib/WebGUI/Test.pm +++ b/t/lib/WebGUI/Test.pm @@ -100,14 +100,16 @@ sub import { if ($ENV{WEBGUI_TEST_DEBUG}) { ##Offset Sessions, and Scratch by 1 because 1 will exist at the start my @checkCount = ( - Sessions => 'userSession', - Scratch => 'userSessionScratch', - Users => 'users', - Groups => 'groups', - mailQ => 'mailQueue', - Tags => 'assetVersionTag', - Assets => 'assetData', - Workflows => 'Workflow', + Sessions => 'userSession', + Scratch => 'userSessionScratch', + Users => 'users', + Groups => 'groups', + mailQ => 'mailQueue', + Tags => 'assetVersionTag', + Assets => 'assetData', + Workflows => 'Workflow', + Carts => 'cart', + 'Ship Drivers' => 'shipper', ); my %initCounts; for ( my $i = 0; $i < @checkCount; $i += 2) { @@ -763,6 +765,8 @@ were passed in. Currently able to destroy: WebGUI::User WebGUI::VersionTag WebGUI::Workflow + WebGUI::Shop::Cart + WebGUI::Shop::ShipDriver Example call: @@ -831,14 +835,20 @@ Example call: ); my %cleanup = ( - 'WebGUI::User' => 'delete', - 'WebGUI::Group' => 'delete', - 'WebGUI::Storage' => 'delete', - 'WebGUI::Shop::Cart' => 'delete', - 'WebGUI::Asset' => 'purge', - 'WebGUI::VersionTag' => 'rollback', - 'WebGUI::Workflow' => 'delete', - 'WebGUI::Session' => sub { + 'WebGUI::User' => 'delete', + 'WebGUI::Group' => 'delete', + 'WebGUI::Storage' => 'delete', + 'WebGUI::Asset' => 'purge', + 'WebGUI::VersionTag' => 'rollback', + 'WebGUI::Workflow' => 'delete', + 'WebGUI::Shop::ShipDriver' => 'delete', + 'WebGUI::Shop::Cart' => sub { + my $cart = shift; + my $addressBook = $cart->getAddressBook(); + $addressBook->delete if $addressBook; ##Should we call cleanupGuard instead??? + $cart->delete; + }, + 'WebGUI::Session' => sub { my $session = shift; $session->var->end; $session->close;