diff --git a/docs/upgrades/upgrade_7.5.2-7.5.3.pl b/docs/upgrades/upgrade_7.5.2-7.5.3.pl index 000c8ba2a..aa827288a 100644 --- a/docs/upgrades/upgrade_7.5.2-7.5.3.pl +++ b/docs/upgrades/upgrade_7.5.2-7.5.3.pl @@ -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; diff --git a/lib/WebGUI/Shop/Tax.pm b/lib/WebGUI/Shop/Tax.pm index 94cadef2f..dda7ef7f0 100644 --- a/lib/WebGUI/Shop/Tax.pm +++ b/lib/WebGUI/Shop/Tax.pm @@ -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. diff --git a/t/Shop/Tax.t b/t/Shop/Tax.t index 0d300996a..ee34afc4b 100644 --- a/t/Shop/Tax.t +++ b/t/Shop/Tax.t @@ -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; } diff --git a/t/supporting_collateral/taxTables/badHeaders.csv b/t/supporting_collateral/taxTables/badHeaders.csv index 36ba113f0..7af14beb6 100644 --- a/t/supporting_collateral/taxTables/badHeaders.csv +++ b/t/supporting_collateral/taxTables/badHeaders.csv @@ -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 diff --git a/t/supporting_collateral/taxTables/badTaxTable.csv b/t/supporting_collateral/taxTables/badTaxTable.csv index bcd03dd13..56f40f257 100644 --- a/t/supporting_collateral/taxTables/badTaxTable.csv +++ b/t/supporting_collateral/taxTables/badTaxTable.csv @@ -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 diff --git a/t/supporting_collateral/taxTables/commentedTaxTable.csv b/t/supporting_collateral/taxTables/commentedTaxTable.csv index 4b20147a0..7e3d32c19 100644 --- a/t/supporting_collateral/taxTables/commentedTaxTable.csv +++ b/t/supporting_collateral/taxTables/commentedTaxTable.csv @@ -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 diff --git a/t/supporting_collateral/taxTables/emptyTaxTable.csv b/t/supporting_collateral/taxTables/emptyTaxTable.csv index 6fdd30d12..d163db1b9 100644 --- a/t/supporting_collateral/taxTables/emptyTaxTable.csv +++ b/t/supporting_collateral/taxTables/emptyTaxTable.csv @@ -1,3 +1,3 @@ -field,value,taxRate +country,state,city,code,taxRate #state,Wisconsin,5.0 #code,53701,0.5 diff --git a/t/supporting_collateral/taxTables/goodTaxTable.csv b/t/supporting_collateral/taxTables/goodTaxTable.csv index bef2e91c7..1f2934110 100644 --- a/t/supporting_collateral/taxTables/goodTaxTable.csv +++ b/t/supporting_collateral/taxTables/goodTaxTable.csv @@ -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 diff --git a/t/supporting_collateral/taxTables/largeTaxTable.csv b/t/supporting_collateral/taxTables/largeTaxTable.csv index 468747574..2168b9cf4 100644 --- a/t/supporting_collateral/taxTables/largeTaxTable.csv +++ b/t/supporting_collateral/taxTables/largeTaxTable.csv @@ -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 diff --git a/t/supporting_collateral/taxTables/missingHeaders.csv b/t/supporting_collateral/taxTables/missingHeaders.csv index f23c5baa8..9bd954afd 100644 --- a/t/supporting_collateral/taxTables/missingHeaders.csv +++ b/t/supporting_collateral/taxTables/missingHeaders.csv @@ -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 diff --git a/t/supporting_collateral/taxTables/orderedTaxTable.csv b/t/supporting_collateral/taxTables/orderedTaxTable.csv index 08a6a69ec..5d2e105f0 100644 --- a/t/supporting_collateral/taxTables/orderedTaxTable.csv +++ b/t/supporting_collateral/taxTables/orderedTaxTable.csv @@ -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