restore fallback to customer billing address for CC transactions, RT#77641, RT#71513
[freeside.git] / FS / FS / cust_main / Location.pm
index bd0af53..3cb73ff 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use vars qw( $DEBUG $me @location_fields );
 use FS::Record qw(qsearch qsearchs);
 use FS::UID qw(dbh);
+use FS::Cursor;
 use FS::cust_location;
 
 use Carp qw(carp);
@@ -16,19 +17,23 @@ BEGIN {
   # set up accessors for location fields
   if (!$init) {
     no strict 'refs';
-    @location_fields = 
-      qw( address1 address2 city county state zip country district
-        latitude longitude coord_auto censustract censusyear geocode
-        addr_clean );
+    @location_fields = qw(
+      locationname
+      address1 address2 city county state zip country
+      district latitude longitude coord_auto censustract censusyear geocode
+      addr_clean
+    );
 
     foreach my $f (@location_fields) {
       *{"FS::cust_main::Location::$f"} = sub {
         carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1);
-        shift->bill_location->$f
+        my $l = shift->bill_location;
+        $l ? $l->$f : '';
       };
       *{"FS::cust_main::Location::ship_$f"} = sub {
         carp "WARNING: tried to set cust_main.ship_$f with accessor" if (@_ > 1);
-        shift->ship_location->$f
+        my $l = shift->ship_location;
+        $l ? $l->$f : '';
       };
     }
     $init++;
@@ -67,7 +72,13 @@ Returns an L<FS::cust_location> object for the customer's billing address.
 sub bill_location {
   my $self = shift;
   $self->hashref->{bill_location} 
-    ||= FS::cust_location->by_key($self->bill_locationnum);
+    ||= FS::cust_location->by_key($self->bill_locationnum)
+    # degraded mode--let the system keep running during upgrades
+    ||  ( $self->get('address1')
+            && FS::cust_location->new({
+                 map { $_ => $self->get($_) } @location_fields
+               })
+        );
 }
 
 =item ship_location
@@ -79,7 +90,19 @@ Returns an L<FS::cust_location> object for the customer's service address.
 sub ship_location {
   my $self = shift;
   $self->hashref->{ship_location}
-    ||= FS::cust_location->by_key($self->ship_locationnum);
+    ||= FS::cust_location->by_key($self->ship_locationnum)
+    # degraded mode--let the system keep running during upgrades
+    ||  ( $self->get('ship_address1')
+            ? FS::cust_location->new({
+                map { $_ => $self->get('ship_'.$_) } @location_fields
+              })
+            : $self->get('address1')
+                ? FS::cust_location->new({
+                    map { $_ => $self->get($_) } @location_fields
+                  })
+                : ''
+        );
+
 }
 
 =item location TYPE
@@ -115,6 +138,8 @@ sub location_fields { @location_fields }
 
 sub _upgrade_data {
   my $class = shift;
+  my %opt = @_;
+
   eval "use FS::contact;
         use FS::contact_class;
         use FS::contact_phone;
@@ -130,6 +155,7 @@ sub _upgrade_data {
     || new FS::contact_class { classname => 'Service'};
 
   if ( !$service_contact_class->classnum ) {
+    warn "Creating service contact class.\n";
     $error = $service_contact_class->insert;
     die "error creating contact class for Service: $error" if $error;
   }
@@ -151,115 +177,249 @@ sub _upgrade_data {
     }
   }
 
-  foreach my $cust_main (qsearch('cust_main', { bill_locationnum => '' })) {
-    # Step 1: extract billing and service addresses into cust_location
-    my $custnum = $cust_main->custnum;
-    my $bill_location = FS::cust_location->new(
-      {
-        custnum => $custnum,
-        map { $_ => $cust_main->get($_) } location_fields()
+  my $num_to_upgrade = FS::cust_main->count('bill_locationnum is null or ship_locationnum is null');
+  my $num_jobs = FS::queue->count('job = \'FS::cust_main::Location::process_upgrade_location\' and status != \'failed\'');
+  if ( $num_to_upgrade > 0 ) {
+    warn "Need to migrate $num_to_upgrade customer locations.\n";
+
+    if ( $opt{queue} ) {
+      if ( $num_jobs > 0 ) {
+        warn "Upgrade already queued.\n";
+      } else {
+        warn "Scheduling upgrade.\n";
+        my $job = FS::queue->new({ job => 'FS::cust_main::Location::process_upgrade_location' });
+        $job->insert;
       }
-    );
-    $error = $bill_location->insert;
-    die "error migrating billing address for customer $custnum: $error"
-      if $error;
+    } else { #do it now
+      process_upgrade_location();
+    }
+
+  }
+
+  # repair an error in earlier upgrades
+  if (!FS::upgrade_journal->is_done('cust_location_censustract_repair')
+       and FS::Conf->new->exists('cust_main-require_censustract') ) {
+
+    foreach my $cust_location (
+      qsearch('cust_location', { 'censustract' => '' })
+    ) {
+      my $custnum = $cust_location->custnum;
+      next if !$custnum; # avoid doing this for prospect locations
+      my $address1 = $cust_location->address1;
+      # find the last history record that had that address
+      my $last_h = qsearchs({
+          table     => 'h_cust_main',
+          extra_sql => " WHERE custnum = $custnum AND address1 = ".
+                        dbh->quote($address1) .
+                        " AND censustract IS NOT NULL",
+          order_by  => " ORDER BY history_date DESC LIMIT 1",
+      });
+      if (!$last_h) {
+        # this is normal; just means it never had a census tract before
+        next;
+      }
+      $cust_location->set('censustract' => $last_h->get('censustract'));
+      $cust_location->set('censusyear'  => $last_h->get('censusyear'));
+      my $error = $cust_location->replace;
+      warn "Error setting census tract for customer #$custnum:\n  $error\n"
+        if $error;
+    } # foreach $cust_location
+    FS::upgrade_journal->set_done('cust_location_censustract_repair');
+  }
+}
+
+sub process_upgrade_location {
+  my $class = shift;
+
+  my $dbh = dbh;
+  local $FS::cust_main::import = 1;
+  local $FS::cust_location::import = 1;
+  local $FS::contact::skip_fuzzyfiles = 1;
+  local $FS::UID::AutoCommit = 0;
+
+  my $tax_prefix = 'bill_';
+  if ( FS::Conf->new->exists('tax-ship_address') ) {
+    $tax_prefix = 'ship_';
+  }
+
+  # load some records that were created during the initial upgrade
+  my $service_contact_class = 
+    qsearchs('contact_class', { classname => 'Service'});
+
+  my %phone_type = (
+    daytime => 'Work',
+    night   => 'Home',
+    mobile  => 'Mobile',
+    fax     => 'Fax'
+  );
+  foreach (keys %phone_type) {
+    $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}});
+  }
+
+  my %opt = (
+    tax_prefix            => $tax_prefix,
+    service_contact_class => $service_contact_class,
+    phone_type            => \%phone_type,
+  );
+
+  my $search = FS::Cursor->new('cust_main',
+                        { bill_locationnum => '',
+                          address1         => { op=>'!=', value=>'' }
+                        });
+  while (my $cust_main = $search->fetch) {
+    my $error = $cust_main->upgrade_location(%opt);
+    if ( $error ) {
+      warn "cust#".$cust_main->custnum.": $error\n";
+      $dbh->rollback;
+    } else {
+      # commit as we go
+      $dbh->commit;
+    }
+  }
+}
+
+sub upgrade_location { # instance method
+  my $cust_main = shift;
+  my %opt = @_;
+  my $error;
 
-    $cust_main->set(bill_locationnum => $bill_location->locationnum);
+  # Step 1: extract billing and service addresses into cust_location
+  my $custnum = $cust_main->custnum;
+  my $bill_location = FS::cust_location->new(
+    {
+      custnum => $custnum,
+      map { $_ => $cust_main->get($_) } location_fields(),
+    }
+  );
+  $bill_location->set('censustract', '');
+  $bill_location->set('censusyear', '');
+   # properly goes with ship_location; if they're the same, will be set
+   # on ship_location before inserting either one
+  my $ship_location = $bill_location; # until proven otherwise
+
+  if ( $cust_main->get('ship_address1') ) {
+    # detect duplicates
+    my $same = 1;
+    foreach (location_fields()) {
+      if ( length($cust_main->get("ship_$_")) and
+           $cust_main->get($_) ne $cust_main->get("ship_$_") ) {
+        $same = 0;
+      }
+    }
 
-    if ( $cust_main->get('ship_address1') ) {
-      # detect duplicates
-      my $same = 1;
-      my $ship_location;
-      foreach (location_fields()) {
-        if ( length($cust_main->get("ship_$_")) and
-             $cust_main->get($_) ne $cust_main->get("ship_$_") ) {
-          $same = 0;
+    if ( !$same ) {
+      $ship_location = FS::cust_location->new(
+        {
+          custnum => $custnum,
+          map { $_ => $cust_main->get("ship_$_") } location_fields()
         }
+      );
+    } # else it stays equal to $bill_location
+
+    # Step 2: Extract shipping address contact fields into contact
+    my %unlike = map { $_ => 1 }
+      grep { $cust_main->get($_) ne $cust_main->get("ship_$_") }
+      qw( last first company daytime night fax mobile );
+
+    if ( %unlike ) {
+      # then there IS a service contact
+      my $contact = FS::contact->new({
+        'custnum'     => $custnum,
+        'classnum'    => $opt{service_contact_class}->classnum,
+        'locationnum' => $ship_location->locationnum,
+        'last'        => $cust_main->get('ship_last'),
+        'first'       => $cust_main->get('ship_first'),
+      });
+      if ( !$cust_main->get('ship_last') or !$cust_main->get('ship_first') )
+      {
+        warn "customer $custnum has no service contact name; substituting ".
+             "customer name\n";
+        $contact->set('last' => $cust_main->get('last'));
+        $contact->set('first' => $cust_main->get('first'));
       }
 
-      if ( $same ) {
-        $ship_location = $bill_location;
-      } else {
-        $ship_location = FS::cust_location->new(
-          {
-            custnum => $custnum,
-            map { $_ => $cust_main->get("ship_$_") } location_fields()
-          }
-        );
-        $error = $ship_location->insert;
-        die "error migrating service address for customer $custnum: $error"
-          if $error;
+      if ( $unlike{'company'} ) {
+        # there's no contact.company field, but keep a record of it
+        $contact->set(comment => 'Company: '.$cust_main->get('ship_company'));
       }
+      $error = $contact->insert;
+      return "error migrating service contact for customer $custnum: $error"
+        if $error;
 
-      $cust_main->set(ship_locationnum => $ship_location->locationnum);
-
-      # Step 2: Extract shipping address contact fields into contact
-      my %unlike = map { $_ => 1 }
-        grep { $cust_main->get($_) ne $cust_main->get("ship_$_") }
-        qw( last first company daytime night fax mobile );
-
-      if ( %unlike ) {
-        # then there IS a service contact
-        my $contact = FS::contact->new({
-          'custnum'     => $custnum,
-          'classnum'    => $service_contact_class->classnum,
-          'locationnum' => $ship_location->locationnum,
-          'last'        => $cust_main->get('ship_last'),
-          'first'       => $cust_main->get('ship_first'),
+      foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) {
+        my $phone = $cust_main->get("ship_$_");
+        next if !$phone;
+        my $contact_phone = FS::contact_phone->new({
+          'contactnum'    => $contact->contactnum,
+          'phonetypenum'  => $opt{phone_type}->{$_}->phonetypenum,
+          FS::contact::_parse_phonestring( $phone )
         });
-        if ( $unlike{'company'} ) {
-          # there's no contact.company field, but keep a record of it
-          $contact->set(comment => 'Company: '.$cust_main->get('ship_company'));
-        }
-        $error = $contact->insert;
-        die "error migrating service contact for customer $custnum: $error"
+        $error = $contact_phone->insert;
+        return "error migrating service contact phone for customer $custnum: $error"
           if $error;
+        $cust_main->set("ship_$_" => '');
+      }
 
-        foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) {
-          my $phone = $cust_main->get("ship_$_");
-          next if !$phone;
-          my $contact_phone = FS::contact_phone->new({
-            'contactnum'    => $contact->contactnum,
-            'phonetypenum'  => $phone_type{$_}->phonetypenum,
-            FS::contact::_parse_phonestring( $phone )
-          });
-          $error = $contact_phone->insert;
-          # die "whose responsible this"
-          die "error migrating service contact phone for customer $custnum: $error"
-            if $error;
-          $cust_main->set("ship_$_" => '');
-        }
+      $cust_main->set("ship_$_" => '') foreach qw(last first company);
+    } #if %unlike
+  } #if ship_address1
 
-        $cust_main->set("ship_$_" => '') foreach qw(last first company);
-      } #if %unlike
-    } #if ship_address1
-    else {
-      $cust_main->set(ship_locationnum => $bill_location->locationnum);
+  # special case: should go with whichever location is used to calculate
+  # taxes, because that's the one it originally came from
+  if ( my $geocode = $cust_main->get('geocode') ) {
+    $bill_location->set('geocode' => '');
+    $ship_location->set('geocode' => '');
+
+    if ( $opt{tax_prefix} eq 'bill_' ) {
+      $bill_location->set('geocode', $geocode);
+    } elsif ( $opt{tax_prefix} eq 'ship_' ) {
+      $ship_location->set('geocode', $geocode);
     }
+  }
 
-    # Step 3: Wipe the migrated fields and update the cust_main
+  # this always goes with the ship_location (whether it's the same as
+  # bill_location or not)
+  $ship_location->set('censustract', $cust_main->get('censustract'));
+  $ship_location->set('censusyear',  $cust_main->get('censusyear'));
 
-    $cust_main->set("ship_$_" => '') foreach location_fields();
-    $cust_main->set($_ => '') foreach location_fields();
+  $error = $bill_location->insert;
+  return "error migrating billing address for customer $custnum: $error"
+    if $error;
 
-    $error = $cust_main->replace;
-    die "error migrating addresses for customer $custnum: $error"
+  $cust_main->set(bill_locationnum => $bill_location->locationnum);
+
+  if (!$ship_location->locationnum) {
+    $error = $ship_location->insert;
+    return "error migrating service address for customer $custnum: $error"
       if $error;
+  }
 
-    # Step 4: set packages at the "default service location" to ship_location
-    foreach my $cust_pkg (
-      qsearch('cust_pkg', { custnum => $custnum, locationnum => '' })  
-    ) {
-      # not a location change
-      $cust_pkg->set('locationnum', $cust_main->ship_locationnum);
-      $error = $cust_pkg->replace;
-      die "error migrating package ".$cust_pkg->pkgnum.": $error"
-        if $error;
-    }
+  $cust_main->set(ship_locationnum => $ship_location->locationnum);
+
+  # Step 3: Wipe the migrated fields and update the cust_main
+
+  $cust_main->set("ship_$_" => '') foreach location_fields();
+  $cust_main->set($_ => '') foreach location_fields();
+
+  $error = $cust_main->replace;
+  return "error migrating addresses for customer $custnum: $error"
+    if $error;
+
+  # Step 4: set packages at the "default service location" to ship_location
+  my $pkg_search =
+    FS::Cursor->new('cust_pkg', { custnum => $custnum, locationnum => '' });
+  while (my $cust_pkg = $pkg_search->fetch) {
+    # not a location change
+    $cust_pkg->set('locationnum', $cust_main->ship_locationnum);
+    $error = $cust_pkg->replace;
+    return "error migrating package ".$cust_pkg->pkgnum.": $error"
+      if $error;
+  }
+  '';
 
-  } #foreach $cust_main
 }
 
+
 =back
 
 =cut