fix duplication of Washington sales taxes, #73185, fallout from #71501
[freeside.git] / FS / FS / geocode_Mixin.pm
index 29491db..09b1131 100644 (file)
@@ -3,7 +3,7 @@ package FS::geocode_Mixin;
 use strict;
 use vars qw( $DEBUG $me );
 use Carp;
 use strict;
 use vars qw( $DEBUG $me );
 use Carp;
-use Locale::Country;
+use Locale::Country ();
 use Geo::Coder::Googlev3; #compile time for now, until others are supported
 use FS::Record qw( qsearchs qsearch );
 use FS::Conf;
 use Geo::Coder::Googlev3; #compile time for now, until others are supported
 use FS::Record qw( qsearchs qsearch );
 use FS::Conf;
@@ -126,25 +126,41 @@ sub location_label {
       $notfirst++;
     }
   }
       $notfirst++;
     }
   }
-  $line .= $separator. &$escape(code2country($self->country))
+  $line .= $separator. &$escape($self->country_full)
     if $self->country ne $cydefault;
 
   $line;
 }
 
     if $self->country ne $cydefault;
 
   $line;
 }
 
-=item set_coord [ PREFIX ]
+=item country_full
+
+Returns the full country name.
+
+=cut
+
+sub country_full {
+  my $self = shift;
+  $self->code2country($self->get('country'));
+}
+
+sub code2country {
+  my( $self, $country ) = @_;
+
+  #a hash?  not expecting an explosion of business from unrecognized countries..
+  return 'KKTC' if $country eq 'XC';
+                                           
+  Locale::Country::code2country($country);
+}
+
+=item set_coord
 
 Look up the coordinates of the location using (currently) the Google Maps
 API and set the 'latitude' and 'longitude' fields accordingly.
 
 
 Look up the coordinates of the location using (currently) the Google Maps
 API and set the 'latitude' and 'longitude' fields accordingly.
 
-PREFIX, if specified, will be prepended to all location field names,
-including latitude and longitude.
-
 =cut
 
 sub set_coord {
   my $self = shift;
 =cut
 
 sub set_coord {
   my $self = shift;
-  my $pre = scalar(@_) ? shift : '';
 
   #my $module = FS::Conf->new->config('geocode_module') || 'Geo::Coder::Googlev3';
 
 
   #my $module = FS::Conf->new->config('geocode_module') || 'Geo::Coder::Googlev3';
 
@@ -152,11 +168,11 @@ sub set_coord {
 
   my $location = eval {
     $geocoder->geocode( location =>
 
   my $location = eval {
     $geocoder->geocode( location =>
-      $self->get($pre.'address1'). ','.
-      ( $self->get($pre.'address2') ? $self->get($pre.'address2').',' : '' ).
-      $self->get($pre.'city'). ','.
-      $self->get($pre.'state'). ','.
-      code2country($self->get($pre.'country'))
+      $self->get('address1'). ','.
+      ( $self->get('address2') ? $self->get('address2').',' : '' ).
+      $self->get('city'). ','.
+      $self->get('state'). ','.
+      $self->country_full
     );
   };
   if ( $@ ) {
     );
   };
   if ( $@ ) {
@@ -166,9 +182,9 @@ sub set_coord {
 
   my $geo_loc = $location->{'geometry'}{'location'} or return;
   if ( $geo_loc->{'lat'} && $geo_loc->{'lng'} ) {
 
   my $geo_loc = $location->{'geometry'}{'location'} or return;
   if ( $geo_loc->{'lat'} && $geo_loc->{'lng'} ) {
-    $self->set($pre.'latitude',  $geo_loc->{'lat'} );
-    $self->set($pre.'longitude', $geo_loc->{'lng'} );
-    $self->set($pre.'coord_auto', 'Y');
+    $self->set('latitude',  $geo_loc->{'lat'} );
+    $self->set('longitude', $geo_loc->{'lng'} );
+    $self->set('coord_auto', 'Y');
   }
 
 }
   }
 
 }
@@ -186,12 +202,17 @@ sub geocode {
   my $geocode = $self->get('geocode');  #XXX only one data_vendor for geocode
   return $geocode if $geocode;
 
   my $geocode = $self->get('geocode');  #XXX only one data_vendor for geocode
   return $geocode if $geocode;
 
-  my $prefix =
-   ( FS::Conf->new->exists('tax-ship_address') && $self->has_ship_address )
-   ? 'ship_'
-   : '';
+  if ( $self->isa('FS::cust_main') ) {
+    warn "WARNING: FS::cust_main->geocode deprecated";
+
+    # do the best we can
+    my $m = FS::Conf->new->exists('tax-ship_address') ? 'ship_location'
+                                                      : 'bill_location';
+    my $location = $self->$m or return '';
+    return $location->geocode($data_vendor);
+  }
 
 
-  my($zip,$plus4) = split /-/, $self->get("${prefix}zip")
+  my($zip,$plus4) = split /-/, $self->get('zip')
     if $self->country eq 'US';
 
   $zip ||= '';
     if $self->country eq 'US';
 
   $zip ||= '';
@@ -212,7 +233,7 @@ sub geocode {
     if scalar(@cust_tax_location);
 
   warn "WARNING: customer ". $self->custnum.
     if scalar(@cust_tax_location);
 
   warn "WARNING: customer ". $self->custnum.
-       ": multiple locations for zip ". $self->get("${prefix}zip").
+       ": multiple locations for zip ". $self->get("zip").
        "; using arbitrary geocode $geocode\n"
     if scalar(@cust_tax_location) > 1;
 
        "; using arbitrary geocode $geocode\n"
     if scalar(@cust_tax_location) > 1;
 
@@ -227,10 +248,14 @@ Queueable function to update the tax district code using the selected method
 
 =cut
 
 
 =cut
 
+# this is run from the job queue so I'm not transactionizing it.
+
 sub process_district_update {
   my $class = shift;
   my $id = shift;
 
 sub process_district_update {
   my $class = shift;
   my $id = shift;
 
+  local $DEBUG = 1;
+
   eval "use FS::Misc::Geo qw(get_district); use FS::Conf; use $class;";
   die $@ if $@;
   die "$class has no location data" if !$class->can('location_hash');
   eval "use FS::Misc::Geo qw(get_district); use FS::Conf; use $class;";
   die $@ if $@;
   die "$class has no location data" if !$class->can('location_hash');
@@ -248,15 +273,55 @@ sub process_district_update {
     my $error = $self->replace;
     die $error if $error;
 
     my $error = $self->replace;
     die $error if $error;
 
-    my %hash = map { $_ => $tax_info->{$_} } 
+    my %hash = map { $_ => uc( $tax_info->{$_} ) } 
       qw( district city county state country );
       qw( district city county state country );
-    my $old = qsearchs('cust_main_county', \%hash);
-    if ( $old ) {
-      my $new = new FS::cust_main_county { $old->hash, %$tax_info };
-      warn "updating tax rate for district ".$tax_info->{'district'} if $DEBUG;
-      $error = $new->replace($old);
-    }
-    else {
+    $hash{'source'} = $method; # apply the update only to taxes we maintain
+
+    my @old = qsearch('cust_main_county', \%hash);
+    if ( @old ) {
+      # prune any duplicates rather than updating them
+      my %keep; # key => cust_main_county record
+      foreach my $cust_main_county (@old) {
+        my $key = join('.', $cust_main_county->city ,
+                            $cust_main_county->district ,
+                            $cust_main_county->taxclass
+                      );
+        if ( exists $keep{$key} ) {
+          my $disable_this = $cust_main_county;
+          # prefer records that have a tax name
+          if ( $cust_main_county->taxname and not $keep{$key}->taxname ) {
+            $disable_this = $keep{$key};
+            $keep{$key} = $cust_main_county;
+          }
+          # disable by setting the rate to zero, and setting source to null
+          # so it doesn't get auto-updated in the future. don't actually 
+          # delete it, that produces orphan records
+          warn "disabling tax rate #" .
+            $disable_this->taxnum .
+            " because it's a duplicate for $key\n"
+            if $DEBUG;
+          # by setting its rate to zero, and never updating
+          # it again
+          $disable_this->set('tax' => 0);
+          $disable_this->set('source' => '');
+          $error = $disable_this->replace;
+          die $error if $error;
+        }
+
+        $keep{$key} ||= $cust_main_county;
+
+      }
+      foreach my $key (keys %keep) {
+        my $cust_main_county = $keep{$key};
+        warn "updating tax rate #".$cust_main_county->taxnum.
+          " for $key" if $DEBUG;
+        # update the tax rate only
+        $cust_main_county->set('tax', $tax_info->{'tax'});
+        $error ||= $cust_main_county->replace;
+      }
+    } else {
+      # make a new tax record, and mark it so we can find it later
+      $tax_info->{'source'} = $method;
       my $new = new FS::cust_main_county $tax_info;
       warn "creating tax rate for district ".$tax_info->{'district'} if $DEBUG;
       $error = $new->insert;
       my $new = new FS::cust_main_county $tax_info;
       warn "creating tax rate for district ".$tax_info->{'district'} if $DEBUG;
       $error = $new->insert;