Change the Shop::Tax system to be hierarchial.

Update all sample tax tables and tests.
This commit is contained in:
Colin Kuskie 2008-03-06 22:34:21 +00:00
parent cbe9cc29df
commit d956e58bd7
11 changed files with 190 additions and 131 deletions

View file

@ -152,11 +152,12 @@ sub insertCommerceTaxTable {
CREATE TABLE tax (
taxId VARCHAR(22) binary NOT NULL,
field VARCHAR(100) NOT NULL,
value VARCHAR(100),
country VARCHAR(100) NOT NULL,
state VARCHAR(100),
city VARCHAR(100),
code VARCHAR(100),
taxRate FLOAT NOT NULL DEFAULT 0.0,
PRIMARY KEY (taxId),
UNIQUE KEY (field, value)
PRIMARY KEY (taxId)
)
EOSQL
@ -168,10 +169,10 @@ sub migrateOldTaxTable {
print "\tMigrate old tax data into the new tax table.\n" unless ($quiet);
# and here's our code
my $oldTax = $session->db->prepare('select * from commerceSalesTax');
my $newTax = $session->db->prepare('insert into tax (taxId, field, value, taxRate) VALUES (?,?,?,?)');
my $newTax = $session->db->prepare('insert into tax (taxId, country, state, city, code, taxRate) VALUES (?,?,?,?,?,?)');
$oldTax->execute();
while (my $oldTaxData = $oldTax->hashRef()) {
$newTax->execute([$oldTaxData->{commerceSalesTaxId}, 'state', $oldTaxData->{regionIdentifier}, $oldTaxData->{salesTax}]);
$newTax->execute([$oldTaxData->{commerceSalesTaxId}, 'USA', $oldTaxData->{regionIdentifier}, '', '', $oldTaxData->{salesTax}]);
}
$oldTax->finish;
$newTax->finish;

View file

@ -20,6 +20,10 @@ This package manages tax information, and calculates taxes on a shopping cart.
in that the only data it contains is a WebGUI::Session object, but it does provide several methods for
handling the information in the tax tables.
Taxes are accumulated through increasingly specific geographic information. For example, you can
specify the sales tax for a whole country, then the additional sales tax for a state in the country,
all the way down to a single code inside of a city.
=head1 SYNOPSIS
use WebGUI::Shop::Tax;
@ -42,40 +46,46 @@ Add tax information to the table. Returns the taxId of the newly created tax in
=head3 $params
A hash ref of the geographic and rate information. All parameters are required and
A hash ref of the geographic and rate information. The country and taxRate parameters
must have defined values.
=head4 field
=head4 country
field denotes what kind of location the tax information is for. This should
be country, state, or code. The combination of field and value is unique
in the database.
The country this tax information applies to.
=head4 value
=head4 state
value is the value of the field to be added. For example, appropriate values
for a field of country might be China, United States, Mexico. If the field
is state, it could be British Colombia, Oregon or Maine.
The state this tax information applies to. state and country together are unique.
=head4 city
The ciy this tax information applies to. Cities are unique with state and country information.
=head4 code
The postal code this tax information applies to. codes are unique with state and country information.
=head4 taxRate
This is the tax rate for the location, as specified by field and value. The tax rate is stored
as a percentage, like 5.5 .
This is the tax rate for the location, as specified by the geographical
fields country, state, city and/or code. The tax rate is stored as
a percentage, like 5.5 .
=cut
sub add {
my $self = shift;
my $params = shift;
my $id = $self->session->id->generate();
WebGUI::Error::InvalidParam->throw(error => 'Must pass in a hashref of params')
unless ref($params) eq 'HASH';
foreach my $key (qw/field value taxRate/) {
WebGUI::Error::InvalidParam->throw(error => "Hash ref must contain a $key key with a defined value")
unless exists($params->{$key}) and defined $params->{$key};
}
$self->session->db->write('insert into tax (taxId, field, value, taxRate) VALUES (?,?,?,?)', [$id, @{ $params }{qw[ field value taxRate ]}]);
WebGUI::Error::InvalidParam->throw(error => "Missing required information.", param => 'country')
unless exists($params->{country}) and $params->{country};
WebGUI::Error::InvalidParam->throw(error => "Missing required information.", param => 'taxRate')
unless exists($params->{taxRate}) and defined $params->{taxRate};
$params->{taxId} = 'new';
my $id = $self->session->db->setRow('tax', 'taxId', $params);
return $id;
}
@ -167,7 +177,7 @@ the file. The file will be named "siteTaxData.csv".
sub exportTaxData {
my $self = shift;
my $taxIterator = $self->getItems;
my @columns = qw{ field value taxRate };
my @columns = grep { $_ ne 'taxId' } $taxIterator->getColumnNames;
my $taxData = WebGUI::Text::joinCSV(@columns) . "\n";
while (my $taxRow = $taxIterator->hashRef() ) {
my @taxData = @{ $taxRow }{@columns};
@ -206,14 +216,26 @@ sub getTaxRates {
my $address = shift;
WebGUI::Error::InvalidObject->throw(error => 'Need an address.', expected=>'WebGUI::Shop::Address', got=>(ref $address))
unless ref($address) eq 'WebGUI::Shop::Address';
my $country = $address->get('country');
my $state = $address->get('state');
my $city = $address->get('city');
my $code = $address->get('code');
my $result = $self->session->db->buildArrayRef(
q{
select taxRate from tax where
(field='state' and value=?)
OR (field='country' and value=?)
OR (field='code' and value=?)
(country=? and state='' and city='' and code='')
OR (country=? and state=? and city='' and code='')
OR (country=? and state=? and city=? and code='')
OR (country=? and state=? and city='' and code=? )
OR (country=? and state=? and city=? and code=? )
},
[$address->get('state'), $address->get('country'), $address->get('code')]);
[
$country,
$country, $state,
$country, $state, $city,
$country, $state, $code,
$country, $state, $city, $code,
]);
return $result;
}
@ -251,8 +273,8 @@ sub importTaxData {
chomp $headers;
my @headers = WebGUI::Text::splitCSV($headers);
WebGUI::Error::InvalidFile->throw(error => qq{Bad header found in the CSV file}, brokenFile => $filePath)
unless (join(q{-}, sort @headers) eq 'field-taxRate-value')
and (scalar @headers == 3);
unless (join(q{-}, sort @headers) eq 'city-code-country-state-taxRate')
and (scalar @headers == 5);
my @taxData = ();
my $line = 1;
while (my $taxRow = <$table>) {
@ -261,7 +283,7 @@ sub importTaxData {
next unless $taxRow;
my @taxRow = WebGUI::Text::splitCSV($taxRow);
WebGUI::Error::InvalidFile->throw(error => qq{Error found in the CSV file}, brokenFile => $filePath, brokenLine => $line)
unless scalar @taxRow == 3;
unless scalar @taxRow == 5;
push @taxData, [ @taxRow ];
}
##Okay, if we got this far, then the data looks fine.

View file

@ -34,7 +34,9 @@ my $session = WebGUI::Test->session;
#----------------------------------------------------------------------------
# Tests
my $tests = 80;
my $addExceptions = getAddExceptions($session);
my $tests = 68 + 2*scalar(@{$addExceptions});
plan tests => 1 + $tests;
#----------------------------------------------------------------------------
@ -88,53 +90,23 @@ $e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'add: correct type of exception thrown for missing hashref');
is($e->error, 'Must pass in a hashref of params', 'add: correct message for a missing hashref');
eval{$taxer->add({})};
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'add: correct type of exception thrown for empty hashref');
is($e->error, 'Hash ref must contain a field key with a defined value', 'add: correct message for an empty hashref');
foreach my $inputSet ( @{ $addExceptions } ){
eval{$taxer->add($inputSet->{args})};
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'add: '.$inputSet->{comment});
cmp_deeply(
$e,
methods(
error => $inputSet->{error},
param => $inputSet->{param},
),
'add: '.$inputSet->{comment},
);
}
my $taxData = {
field => undef,
};
eval{$taxer->add($taxData)};
like($@, qr{},
'add: error handling for undefined field key');
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'add: correct type of exception thrown for valueless hash key');
is($e->error, 'Hash ref must contain a field key with a defined value', 'add: correct message for valueless hash key');
$taxData->{field} = 'state';
eval{$taxer->add($taxData)};
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'add: correct type of exception thrown for missing field hash key');
is($e->error, 'Hash ref must contain a value key with a defined value', 'add: correct message for missing field hash key');
$taxData->{value} = undef;
eval{$taxer->add($taxData)};
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'add: correct type of exception thrown for missing hash key');
is($e->error, 'Hash ref must contain a value key with a defined value', 'add: correct message for missing field hash value');
$taxData->{value} = 'Oregon';
eval{$taxer->add($taxData)};
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'add: correct type of exception thrown for missing hash key');
is($e->error, 'Hash ref must contain a taxRate key with a defined value', 'add: correct message for missing taxRate hash key');
$taxData->{taxRate} = undef;
eval{$taxer->add($taxData)};
$e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'add: correct type of exception thrown for missing hash value');
is($e->error, 'Hash ref must contain a taxRate key with a defined value', 'add: correct message for missing taxRate hash value');
my $taxData = {
field => 'state',
value => 'Oregon',
country => 'USA',
state => 'OR',
taxRate => '0',
};
@ -147,12 +119,16 @@ is($taxIterator->rows, 1, 'add added only 1 row to the tax table');
my $addedData = $taxIterator->hashRef;
$taxData->{taxId} = $oregonTaxId;
$taxData->{city} = undef;
$taxData->{code} = undef;
cmp_deeply($taxData, $addedData, 'add put the right data into the database for Oregon');
cmp_deeply($addedData, $taxData, 'add put the right data into the database for Oregon');
$taxData = {
field => 'state',
value => 'Wisconsin',
country => 'USA',
state => 'Wisconsin',
city => 'Madcity',
code => '53702',
taxRate => '5',
};
@ -162,18 +138,15 @@ $taxIterator = $taxer->getItems;
is($taxIterator->rows, 2, 'add added another row to the tax table');
$taxData = {
field => 'state',
value => 'Oregon',
country => 'state',
state => 'Oregon',
taxRate => '0.1',
};
eval {$taxer->add($taxData)};
##This error is thrown by DBI, not us.
ok($@, 'add threw an exception to having taxes in Oregon when they were defined as 0 initially');
my $dupId = $taxer->add($taxData);
$taxIterator = $taxer->getItems;
is($taxIterator->rows, 2, 'add did not add another row since it would be a duplicate');
is($taxIterator->rows, 3, 'add permits adding duplicate information.');
##Madison zip codes:
##53701-53709
@ -201,10 +174,13 @@ $e = Exception::Class->caught();
isa_ok($e, 'WebGUI::Error::InvalidParam', 'delete: error handling for an undefined taxId value');
is($e->error, 'Hash ref must contain a taxId key with a defined value', 'delete: error message for an undefined taxId value');
$taxer->delete({ taxId => $oregonTaxId });
$taxer->delete({ taxId => $dupId });
$taxIterator = $taxer->getItems;
is($taxIterator->rows, 1, 'One row was deleted from the tax table');
is($taxIterator->rows, 2, 'One row was deleted from the tax table, even though another row has duplicate information');
$taxer->delete({ taxId => $oregonTaxId });
$taxIterator = $taxer->getItems;
is($taxIterator->rows, 1, 'Another row was deleted from the tax table');
$taxer->delete({ taxId => $session->id->generate });
@ -212,11 +188,11 @@ $taxIterator = $taxer->getItems;
is($taxIterator->rows, 1, 'No rows were deleted from the table since the requested id does not exist');
is($taxIterator->hashRef->{taxId}, $wisconsinTaxId, 'The correct tax information was deleted');
#######################################################################
#
# exportTaxData
#
#######################################################################
########################################################################
##
## exportTaxData
##
########################################################################
$storage = $taxer->exportTaxData();
isa_ok($storage, 'WebGUI::Storage', 'exportTaxData returns a WebGUI::Storage object');
@ -226,7 +202,7 @@ cmp_ok($storage->getFileSize('siteTaxData.csv'), '!=', 0, 'CSV file is not empty
my @fileLines = split /\n+/, $storage->getFileContentsAsScalar('siteTaxData.csv');
#my @fileLines = ();
my @header = WebGUI::Text::splitCSV($fileLines[0]);
my @expectedHeader = qw/field value taxRate/;
my @expectedHeader = qw/country state city code taxRate/;
cmp_deeply(\@header, \@expectedHeader, 'exportTaxData: header line is correct');
my @row1 = WebGUI::Text::splitCSV($fileLines[1]);
my $wiData = $taxer->getItems->hashRef;
@ -283,13 +259,24 @@ SKIP: {
my $expectedTaxData = [
{
field => 'state',
value => 'Wisconsin',
taxRate => 5.0,
country => 'USA',
state => '',
city => '',
code => '',
taxRate => 0,
},
{
field => 'code',
value => 53701,
country => 'USA',
state => 'Wisconsin',
city => '',
code => '',
taxRate => 5,
},
{
country => 'USA',
state => 'Wisconsin',
city => 'Madison',
code => '53701',
taxRate => 0.5,
},
];
@ -302,7 +289,7 @@ ok(
);
$taxIterator = $taxer->getItems;
is($taxIterator->rows, 2, 'import: Old data deleted, new data imported');
is($taxIterator->rows, 3, 'import: Old data deleted, new data imported');
my @goodTaxData = _grabTaxData($taxIterator);
cmp_bag(
\@goodTaxData,
@ -318,7 +305,7 @@ ok(
);
$taxIterator = $taxer->getItems;
is($taxIterator->rows, 2, 'import: Old data deleted, new data imported again');
is($taxIterator->rows, 3, 'import: Old data deleted, new data imported again');
my @orderedTaxData = _grabTaxData($taxIterator);
cmp_bag(
\@orderedTaxData,
@ -334,7 +321,7 @@ ok(
);
$taxIterator = $taxer->getItems;
is($taxIterator->rows, 2, 'import: Old data deleted, new data imported the third time');
is($taxIterator->rows, 3, 'import: Old data deleted, new data imported the third time');
my @orderedTaxData = _grabTaxData($taxIterator);
cmp_bag(
\@orderedTaxData,
@ -349,6 +336,9 @@ ok(
'Empty tax data not inserted',
);
$taxIterator = $taxer->getItems;
is($taxIterator->rows, 3, 'import: Old data still exists and was not deleted');
my $failure;
eval {
$failure = $taxer->importTaxData(
@ -443,7 +433,7 @@ cmp_deeply(
cmp_deeply(
$taxer->getTaxRates($taxingAddress),
[5, 0.5],
[0, 5, 0.5],
'getTaxRates: return correct data for a state with tax data'
);
@ -541,6 +531,42 @@ sub _grabTaxData {
return @taxData;
}
sub getAddExceptions {
my $session = shift;
my $inputValidion = [
{
args => {},
error => q{Missing required information.},
param => q{country},
comment => q{missing country},
},
{
args => {country => undef},
error => q{Missing required information.},
param => q{country},
comment => q{undef country},
},
{
args => {country => ''},
error => q{Missing required information.},
param => q{country},
comment => q{empty country},
},
{
args => {country => 'USA'},
error => q{Missing required information.},
param => q{taxRate},
comment => q{missing taxRate},
},
{
args => {country => 'USA', taxRate => undef},
error => q{Missing required information.},
param => q{taxRate},
comment => q{empty taxRate},
},
];
}
#----------------------------------------------------------------------------
# Cleanup
END {
@ -548,5 +574,5 @@ END {
$session->db->write('delete from cart');
$session->db->write('delete from addressBook');
$session->db->write('delete from address');
$storage->delete;
#$storage->delete;
}

View file

@ -1,3 +1,7 @@
country,state,city,zip,taxRate
USA,,,,0.0
USA,Wisconsin,,,5.0
USA,Wisconsin,Madison,53701,0.5
where,value,taxRates
state,5.0
code,53701,0.5

1 where,value,taxRates country,state,city,zip,taxRate
1 country,state,city,zip,taxRate
2 USA,,,,0.0
3 USA,Wisconsin,,,5.0
4 USA,Wisconsin,Madison,53701,0.5
5 where,value,taxRates where,value,taxRates
6 state,5.0 state,5.0
7 code,53701,0.5 code,53701,0.5

View file

@ -1,3 +1,4 @@
field,value,taxRate
state,5.0
code,53701,0.5
country,state,city,code,taxRate
USA,,,,0.0,
USA,Wisconsin,,,5.0
USA,Wisconsin,Madison,53701,0.5

1 field,value,taxRate country,state,city,code,taxRate
2 state,5.0 USA,,,,0.0,
3 code,53701,0.5 USA,Wisconsin,,,5.0
4 USA,Wisconsin,Madison,53701,0.5

View file

@ -1,8 +1,9 @@
taxRate,value,field
country,state,city,code,taxRate
#header lines above
#This is just a zip code.
0.5,53701,code
#This is just a country.
USA,,,,0.0
USA,Wisconsin,,,5.0 #Wisconsin is expensive
5.0,Wisconsin,state #Wisconsin is expensive!
USA,Wisconsin,Madison,53701,0.5

1 taxRate,value,field country,state,city,code,taxRate
2 #header lines above #header lines above
3 #This is just a zip code. #This is just a country.
4 0.5,53701,code USA,,,,0.0
5 5.0,Wisconsin,state #Wisconsin is expensive! USA,Wisconsin,,,5.0 #Wisconsin is expensive
6 USA,Wisconsin,Madison,53701,0.5
7
8
9

View file

@ -1,3 +1,3 @@
field,value,taxRate
country,state,city,code,taxRate
#state,Wisconsin,5.0
#code,53701,0.5

1 field country,state,city,code,taxRate value taxRate
2 #state #state,Wisconsin,5.0 Wisconsin 5.0
3 #code #code,53701,0.5 53701 0.5

View file

@ -1,3 +1,4 @@
field,value,taxRate
state,Wisconsin,5.0
code,53701,0.5
country,state,city,code,taxRate
USA,,,,0.0
USA,Wisconsin,,,5.0
USA,Wisconsin,Madison,53701,0.5

1 field country value state city code taxRate
2 state USA Wisconsin 5.0 0.0
3 code USA 53701 Wisconsin 0.5 5.0
4 USA Wisconsin Madison 53701 0.5

View file

@ -1,8 +1,9 @@
field,value,taxRate
state,WI,5.0
code,53701,0.5
code,53702,0.5
code,53703,0.5
code,53704,0.5
state,CA,7.25
code,97123,0.0
country,state,city,code,taxRate
USA,,,,0.0
USA,WI,,,5.0
USA,WI,,53701,0.5
USA,WI,,53702,0.5
USA,WI,,53703,0.5
USA,WI,,53704,0.5
USA,CA,,,7.25
USA,,,97123,0.0

1 field country value state city code taxRate
2 state USA WI 5.0 0.0
3 code USA 53701 WI 0.5 5.0
4 code USA 53702 WI 53701 0.5
5 code USA 53703 WI 53702 0.5
6 code USA 53704 WI 53703 0.5
7 state USA CA WI 53704 7.25 0.5
8 code USA 97123 CA 0.0 7.25
9 USA 97123 0.0

View file

@ -1,3 +1,4 @@
field,taxRate
state,5.0
code,53701,0.5
country,city,code,taxRate
USA,,,,0.0
USA,Wisconsin,,,5.0
USA,Wisconsin,Madison,53701,0.5

1 field,taxRate country,city,code,taxRate
2 state,5.0 USA,,,,0.0
3 code,53701,0.5 USA,Wisconsin,,,5.0
4 USA,Wisconsin,Madison,53701,0.5

View file

@ -1,3 +1,4 @@
taxRate,value,field
5.0,Wisconsin,state
0.5,53701,code
taxRate,country,state,city,code
0.0,USA,,,
5.0,USA,Wisconsin,,
0.5,USA,Wisconsin,Madison,53701

1 taxRate value country field state city code
2 5.0 0.0 Wisconsin USA state
3 0.5 5.0 53701 USA code Wisconsin
4 0.5 USA Wisconsin Madison 53701