Change the Shop::Tax system to be hierarchial.
Update all sample tax tables and tests.
This commit is contained in:
parent
cbe9cc29df
commit
d956e58bd7
11 changed files with 190 additions and 131 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
176
t/Shop/Tax.t
176
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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,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,3 +1,3 @@
|
|||
field,value,taxRate
|
||||
country,state,city,code,taxRate
|
||||
#state,Wisconsin,5.0
|
||||
#code,53701,0.5
|
||||
|
|
|
|||
|
|
|
@ -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,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,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,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
|
||||
|
|
|
|||
|
Loading…
Add table
Add a link
Reference in a new issue