summaryrefslogtreecommitdiff
path: root/FS/t
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2016-11-07 16:24:16 -0800
committerMark Wells <mark@freeside.biz>2016-11-07 16:26:05 -0800
commitdc4e882662ac72279c008d47903a3978cf227f72 (patch)
treef69e53c3a1532ed60e828b9dab7730018b1f60ea /FS/t
parent52c4764eafbb02a1ee74f983ece53b07306e1dde (diff)
revise process for updating WA sales taxes, #73185 and #73226
Conflicts: FS/FS/Conf.pm
Diffstat (limited to 'FS/t')
-rwxr-xr-xFS/t/suite/12-wa_sales_tax.t232
1 files changed, 232 insertions, 0 deletions
diff --git a/FS/t/suite/12-wa_sales_tax.t b/FS/t/suite/12-wa_sales_tax.t
new file mode 100755
index 000000000..473c9a7ba
--- /dev/null
+++ b/FS/t/suite/12-wa_sales_tax.t
@@ -0,0 +1,232 @@
+#!/usr/bin/perl
+
+=head2 DESCRIPTION
+
+Tests automatic lookup of Washington sales tax districts and rates.
+
+This will set up two tax classes. One of them (class A) has only the sales
+tax. The other (class B) will have an additional, manually created tax.
+
+This will test the following sequence of actions (running
+process_district_update() after each one):
+
+1. Enter a customer in Washington for which there is not yet a district tax
+ entry.
+2. Add a manual tax in class B.
+3. Rename the sales taxes.
+4. Delete the sales taxes.
+5. Change the sales tax rates (to simulate a change in the actual rate).
+6. Set the sales tax rate to zero.
+
+The correct result is always for there to be exactly one tax entry for this
+district in each class, with the correct rate, except after step 6, when
+the rate should remain at zero (because setting the rate to zero is a way
+of manually disabling the tax).
+
+=cut
+
+use strict;
+use Test::More tests => 6;
+use FS::Test;
+use FS::cust_main;
+use FS::cust_location;
+use FS::cust_main_county;
+use FS::Misc;
+use FS::Conf;
+my $FS= FS::Test->new;
+
+my $error;
+
+# start clean
+my @taxes = $FS->qsearch('cust_main_county', { city => 'SEATTLE' });
+my @classes = $FS->qsearch('part_pkg_taxclass');
+foreach (@taxes, @classes) {
+ $error = $_->delete;
+ BAIL_OUT("can't flush existing taxes: $error") if $error;
+ # we won't charge any of the taxes in this script so FK errors shouldn't
+ # happen.
+}
+
+# configure stuff
+@classes = map { FS::part_pkg_taxclass->new({ taxclass => $_ }) }
+ qw( ClassA ClassB );
+foreach (@classes) {
+ $error = $_->insert;
+ BAIL_OUT("can't create tax class: $error") if $error;
+}
+
+# should be an FS::Test method to temporarily set this up
+my $conf = FS::Conf->new;
+$conf->set('tax_district_method', 'wa_sales');
+$conf->set('tax_district_taxname', 'Sales Tax');
+$conf->set('enable_taxclasses', 1);
+
+# create the customer
+my $cust = $FS->new_customer('WA Taxes');
+# Sea-Tac International Airport
+$cust->bill_location->address1('17801 International Blvd');
+$cust->bill_location->city('Seattle');
+$cust->bill_location->zip('98158');
+$cust->bill_location->state('WA');
+$cust->bill_location->country('US');
+
+$error = $cust->insert;
+BAIL_OUT("can't create test customer: $error") if $error;
+
+my $location = $cust->bill_location;
+my @prev_taxes;
+
+# after each action, refresh the tax district (as if we'd added/edited a
+# customer in that district) and then get the new list of defined taxes
+sub reprocess {
+ # remember all the taxes from the last test
+ @prev_taxes = map { $_ && FS::cust_main_county->new({$_->hash}) } @taxes;
+ local $@;
+ eval { FS::geocode_Mixin::process_district_update( 'FS::cust_location',
+ $location->locationnum )};
+ $error = $@;
+ BAIL_OUT("can't update tax district: $error") if $error;
+
+ $location = $location->replace_old;
+ @taxes = $FS->qsearch({
+ table => 'cust_main_county',
+ hashref => { city => 'SEATTLE' },
+ order_by => 'ORDER BY taxclass ASC, taxname ASC', # make them easily findable
+ });
+}
+
+# and then we'll want to check that the total number of taxes is what we
+# expect.
+sub ok_num_taxes {
+ my $num = shift;
+ is( scalar(@taxes), $num, "Number of taxes" )
+ or BAIL_OUT('Wrong number of tax records, can\'t continue.');
+}
+
+subtest 'Step 1: Initial tax lookup' => sub {
+ plan 'tests' => 4;
+ reprocess();
+ ok( $location->district, 'Found district '.$location->district);
+ ok_num_taxes(2);
+ ok( ( $taxes[0]
+ and $taxes[0]->taxname eq 'Sales Tax'
+ and $taxes[0]->taxclass eq 'ClassA'
+ and $taxes[0]->district eq $location->district
+ and $taxes[0]->source eq 'wa_sales'
+ and $taxes[0]->tax > 0
+ ),
+ 'ClassA tax = '.$taxes[0]->tax )
+ or diag explain($taxes[0]);
+ ok( ( $taxes[1]
+ and $taxes[1]->taxname eq 'Sales Tax'
+ and $taxes[1]->taxclass eq 'ClassB'
+ and $taxes[1]->district eq $location->district
+ and $taxes[1]->source eq 'wa_sales'
+ and $taxes[1]->tax > 0
+ ),
+ 'ClassB tax = '.$taxes[1]->tax )
+ or diag explain($taxes[1]);
+};
+
+# "Sales Tax" sorts before "USF"; this is intentional.
+subtest 'Step 2: Add manual tax ("USF") to ClassB' => sub {
+ plan tests => 4;
+ if ($taxes[1]) {
+ my $manual_tax = $taxes[1]->new({
+ $taxes[1]->hash,
+ 'taxnum' => '',
+ 'taxname' => 'USF',
+ 'source' => '',
+ 'tax' => '17',
+ });
+ $error = $manual_tax->insert;
+ BAIL_OUT("can't create manual tax: $error") if $error;
+ }
+ reprocess();
+ ok_num_taxes(3);
+ is_deeply( $taxes[0], $prev_taxes[0], 'ClassA sales tax was not changed' );
+ is_deeply( $taxes[1], $prev_taxes[1], 'ClassB sales tax was not changed' );
+ ok( ( $taxes[2]
+ and $taxes[2]->taxname eq 'USF'
+ and $taxes[2]->taxclass eq 'ClassB'
+ and $taxes[2]->tax == 17
+ and $taxes[2]->source eq ''
+ ), 'Manual tax was accepted')
+ or diag explain($taxes[2]);
+};
+
+subtest 'Step 3: Rename ClassB sales tax. Does it stay renamed?' => sub {
+ plan tests => 4;
+ if ($taxes[1]) {
+ $taxes[1]->set('taxname', 'Renamed Sales Tax');
+ $error = $taxes[1]->replace;
+ BAIL_OUT("can't rename tax: $error") if $error;
+ }
+
+ reprocess();
+ ok_num_taxes(3);
+ is_deeply( $taxes[0], $prev_taxes[0], 'ClassA sales tax was not changed' );
+ ok( ( $taxes[1]
+ and $taxes[1]->taxname eq 'Renamed Sales Tax'
+ and $taxes[1]->source eq 'wa_sales'
+ and $taxes[1]->tax == $prev_taxes[1]->tax
+ ), $taxes[1]->taxclass .' sales tax was renamed')
+ or diag explain($taxes[1]);
+ is_deeply( $taxes[2], $prev_taxes[2], 'ClassB manual tax was not changed' );
+};
+
+subtest 'Step 4: Remove ClassB sales tax. Is it recreated?' => sub {
+ plan tests => 4;
+ if ($taxes[1]) {
+ $error = $taxes[1]->delete;
+ BAIL_OUT("can't delete tax: $error") if $error;
+ }
+ reprocess();
+ ok_num_taxes(3);
+ is_deeply( $taxes[0], $prev_taxes[0], 'ClassA sales tax was not changed' );
+ ok( ( $taxes[1]
+ and $taxes[1]->taxname eq 'Sales Tax'
+ and $taxes[1]->source eq 'wa_sales'
+ and $taxes[1]->tax == $prev_taxes[1]->tax
+ ), $taxes[1]->taxclass .' sales tax was deleted and recreated')
+ or diag explain($taxes[1]);
+ is_deeply( $taxes[2], $prev_taxes[2], 'ClassB manual tax was not changed' );
+};
+
+subtest 'Step 5: Simulate a change in tax rate. Do the taxes update?' => sub {
+ plan tests => 3;
+ my $correct_rate = $taxes[0]->tax;
+ foreach (@taxes[0,1]) {
+ if ($_ and $_->source eq 'wa_sales') {
+ $_->tax( $correct_rate + 1 );
+ $error = $_->replace;
+ BAIL_OUT("can't change tax rate: $error") if $error;
+ }
+ }
+ reprocess();
+ ok_num_taxes(3);
+ ok( ( $taxes[0] and $taxes[0]->tax == $correct_rate
+ and $taxes[1] and $taxes[1]->tax == $correct_rate
+ ), 'Tax was reset to correct rate' )
+ or diag("Tax rates: ".$taxes[0]->tax.', '.$taxes[1]->tax);
+ is_deeply( $taxes[2], $prev_taxes[2], 'ClassB manual tax was not changed' );
+};
+
+subtest 'Step 6: Manually disable Class A sales tax. Does it stay disabled?' => sub {
+ plan tests => 4;
+ if ($taxes[0]) {
+ $taxes[0]->set('tax', 0);
+ $error = $taxes[0]->replace;
+ BAIL_OUT("can't change tax rate to zero: $error") if $error;
+ }
+ reprocess();
+ ok_num_taxes(3);
+ ok( $taxes[0]->tax == 0, 'ClassA sales tax remains at zero')
+ or diag("Tax rate: ".$taxes[1]->tax);
+ is_deeply( $taxes[1], $prev_taxes[1], 'ClassB sales tax was not changed' );
+ is_deeply( $taxes[2], $prev_taxes[2], 'ClassB manual tax was not changed' );
+};
+
+$conf->delete('tax_district_method');
+$conf->delete('tax_district_taxname');
+$conf->delete('enable_taxclasses');