compatibility fix for FFIEC census tract lookup, #25258
[freeside.git] / FS / FS / Misc / Geo.pm
index 3031b65..e5355cc 100644 (file)
@@ -6,12 +6,14 @@ use vars qw( $DEBUG @EXPORT_OK );
 use LWP::UserAgent;
 use HTTP::Request;
 use HTTP::Request::Common qw( GET POST );
+use HTTP::Cookies;
 use HTML::TokeParser;
+use URI::Escape 3.31;
 use Data::Dumper;
 
-$DEBUG = 1;
+$DEBUG = 0;
 
-@EXPORT_OK = qw( get_censustract );
+@EXPORT_OK = qw( get_censustract get_district );
 
 =head1 NAME
 
@@ -21,7 +23,7 @@ FS::Misc::Geo - routines to fetch geographic information
 
 =over 4
 
-=item censustract LOCATION YEAR
+=item get_censustract LOCATION YEAR
 
 Given a location hash (see L<FS::location_Mixin>) and a census map year,
 returns a census tract code (consisting of state, county, and tract 
@@ -35,18 +37,18 @@ sub get_censustract {
 
   warn Dumper($location, $year) if $DEBUG;
 
-  my $url='http://www.ffiec.gov/Geocode/default.aspx';
+  my $url = 'http://www.ffiec.gov/Geocode/default.aspx';
 
   my $return = {};
   my $error = '';
 
-  my $ua = new LWP::UserAgent;
+  my $ua = new LWP::UserAgent('cookie_jar' => HTTP::Cookies->new);
   my $res = $ua->request( GET( $url ) );
 
   warn $res->as_string
     if $DEBUG > 1;
 
-  unless ($res->code  eq '200') {
+  if (!$res->is_success) {
 
     $error = $res->message;
 
@@ -66,7 +68,7 @@ sub get_censustract {
       last if $viewstate && $eventvalidation;
     }
 
-    unless ($viewstate && $eventvalidation ) {
+    if (!$viewstate or !$eventvalidation ) {
 
       $error = "either no __VIEWSTATE or __EVENTVALIDATION found";
 
@@ -74,14 +76,12 @@ sub get_censustract {
 
       my($zip5, $zip4) = split('-',$location->{zip});
 
-      $year ||= '2011';
-      #ugh  workaround a mess at ffiec
-      $year = " $year" if $year ne '2011';
+      $year ||= '2012';
       my @ffiec_args = (
         __VIEWSTATE => $viewstate,
         __EVENTVALIDATION => $eventvalidation,
+        __VIEWSTATEENCRYPTED => '',
         ddlbYear    => $year,
-        ddlbYear    => '2011', #' 2009',
         txtAddress  => $location->{address1},
         txtCity     => $location->{city},  
         ddlbState   => $location->{state},
@@ -116,7 +116,10 @@ sub get_censustract {
           $return->{lc($1)} = $p->get_trimmed_text("/span");
         }
 
-        $error = "No census tract found" unless $return->{tractcode};
+        unless ( $return->{tractcode} ) {
+          warn "$error: $content ". Dumper($return) if $DEBUG;
+          $error = "No census tract found";
+        }
         $return->{tractcode} .= ' '
           unless $error || $JSON::VERSION >= 2; #broken JSON 1 workaround
 
@@ -131,4 +134,144 @@ sub get_censustract {
   $return->{'statecode'} .  $return->{'countycode'} .  $return->{'tractcode'};
 }
 
+sub get_district_methods {
+  ''         => '',
+  'wa_sales' => 'Washington sales tax',
+};
+
+=item get_district LOCATION METHOD
+
+For the location hash in LOCATION, using lookup method METHOD, fetch
+tax district information.  Currently the only available method is 
+'wa_sales' (the Washington Department of Revenue sales tax lookup).
+
+Returns a hash reference containing the following fields:
+
+- district
+- tax (percentage)
+- taxname
+- exempt_amount (currently zero)
+- city, county, state, country (from 
+
+The intent is that you can assign this to an L<FS::cust_main_county> 
+object and insert it if there's not yet a tax rate defined for that 
+district.
+
+get_district will die on error.
+
+=over 4
+
+=cut
+
+sub get_district {
+  no strict 'refs';
+  my $location = shift;
+  my $method = shift or return '';
+  warn Dumper($location, $method) if $DEBUG;
+  &$method($location);
+}
+
+sub wa_sales {
+  my $location = shift;
+  my $error = '';
+  return '' if $location->{state} ne 'WA';
+
+  my $return = { %$location };
+  $return->{'exempt_amount'} = 0.00;
+
+  my $url = 'http://webgis2.dor.wa.gov/TaxRateLookup_AGS/TaxReport.aspx';
+  my $ua = new LWP::UserAgent;
+
+  my $delim = '<|>'; # yes, <|>
+  my $year  = (localtime)[5] + 1900;
+  my $month = (localtime)[4] + 1;
+  my @zip = split('-', $location->{zip});
+
+  my @args = (
+    'TaxType=S',  #sales; 'P' = property
+    'Src=0',      #does something complicated
+    'TAXABLE=',
+    'Addr='.uri_escape($location->{address1}),
+    'City='.uri_escape($location->{city}),
+    'Zip='.$zip[0],
+    'Zip1='.($zip[1] || ''), #optional
+    'Year='.$year,
+    'SYear='.$year,
+    'Month='.$month,
+    'EMon='.$month,
+  );
+  
+  my $query_string = join($delim, @args );
+  $url .= "?$query_string";
+  warn "\nrequest:  $url\n\n" if $DEBUG;
+
+  my $res = $ua->request( GET( "$url?$query_string" ) );
+
+  warn $res->as_string
+  if $DEBUG > 1;
+
+  if ($res->code ne '200') {
+    $error = $res->message;
+  }
+
+  my $content = $res->content;
+  my $p = new HTML::TokeParser \$content;
+  my $js = '';
+  while ( my $t = $p->get_tag('script') ) {
+    my $u = $p->get_token; #either enclosed text or the </script> tag
+    if ( $u->[0] eq 'T' and $u->[1] =~ /tblSales/ ) {
+      $js = $u->[1];
+      last;
+    }
+  }
+  if ( $js ) { #found it
+    # strip down to the quoted string, which contains escaped single quotes.
+    $js =~ s/.*\('tblSales'\);c.innerHTML='//s;
+    $js =~ s/(?<!\\)'.*//s; # (?<!\\) means "not preceded by a backslash"
+    warn "\n\n  innerHTML:\n$js\n\n" if $DEBUG > 2;
+
+    $p = new HTML::TokeParser \$js;
+    TD: while ( my $td = $p->get_tag('td') ) {
+      while ( my $u = $p->get_token ) {
+        next TD if $u->[0] eq 'E' and $u->[1] eq 'td';
+        next if $u->[0] ne 'T'; # skip non-text
+        my $text = $u->[1];
+
+        if ( lc($text) eq 'location code' ) {
+          $p->get_tag('td'); # skip to the next column
+          undef $u;
+          $u = $p->get_token until $u->[0] eq 'T'; # and then skip non-text
+          $return->{'district'} = $u->[1];
+        }
+        elsif ( lc($text) eq 'total tax rate' ) {
+          $p->get_tag('td');
+          undef $u;
+          $u = $p->get_token until $u->[0] eq 'T';
+          $return->{'tax'} = $u->[1];
+        }
+      } # get_token
+    } # TD
+
+    # just to make sure
+    if ( $return->{'district'} =~ /^\d+$/ and $return->{'tax'} =~ /^.\d+$/ ) {
+      $return->{'tax'} *= 100; #percentage
+      warn Dumper($return) if $DEBUG;
+      return $return;
+    }
+    else {
+      $error = 'district code/tax rate not found';
+    }
+  }
+  else {
+    $error = "failed to parse document";
+  }
+
+  die "WA tax district lookup error: $error";
+}
+
+=back
+
+=cut
+
+
 1;