agent-virtualize credit card surcharge percentage, RT#72961
[freeside.git] / FS / FS / contact.pm
index 437fd16..fd3e9d7 100644 (file)
@@ -1,14 +1,19 @@
 package FS::contact;
 package FS::contact;
-use base qw( FS::Record );
+use base qw( FS::Password_Mixin
+             FS::Record );
 
 use strict;
 use vars qw( $skip_fuzzyfiles );
 
 use strict;
 use vars qw( $skip_fuzzyfiles );
+use Carp;
 use Scalar::Util qw( blessed );
 use FS::Record qw( qsearch qsearchs dbh );
 use Scalar::Util qw( blessed );
 use FS::Record qw( qsearch qsearchs dbh );
+use FS::Cursor;
 use FS::contact_phone;
 use FS::contact_email;
 use FS::queue;
 use FS::phone_type; #for cgi_contact_fields
 use FS::contact_phone;
 use FS::contact_email;
 use FS::queue;
 use FS::phone_type; #for cgi_contact_fields
+use FS::cust_contact;
+use FS::prospect_contact;
 
 $skip_fuzzyfiles = 0;
 
 
 $skip_fuzzyfiles = 0;
 
@@ -85,7 +90,6 @@ empty or bcrypt
 
 disabled
 
 
 disabled
 
-
 =back
 
 =head1 METHODS
 =back
 
 =head1 METHODS
@@ -108,6 +112,26 @@ sub table { 'contact'; }
 Adds this record to the database.  If there is an error, returns the error,
 otherwise returns false.
 
 Adds this record to the database.  If there is an error, returns the error,
 otherwise returns false.
 
+If the object has an C<emailaddress> field, L<FS::contact_email> records
+will be created for each (comma-separated) email address in that field. If
+any of these coincide with an existing email address, this contact will be
+merged with the contact with that address.
+
+Then, if the object has any fields named C<phonetypenumN> an
+L<FS::contact_phone> record will be created for each of them. Those fields
+should contain phone numbers of the appropriate types (where N is the key of
+an L<FS::phone_type> record identifying the type of number: daytime, night,
+etc.).
+
+After inserting the record, if the object has a 'custnum' or 'prospectnum'
+field, an L<FS::cust_contact> or L<FS::prospect_contact> record will be
+created to link the contact to the customer. The following fields will also
+be included in that record, if they are set on the object:
+- classnum
+- comment
+- selfservice_access
+- invoice_dest
+
 =cut
 
 sub insert {
 =cut
 
 sub insert {
@@ -123,23 +147,111 @@ sub insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $error = $self->SUPER::insert;
+  #save off and blank values that move to cust_contact / prospect_contact now
+  my $prospectnum = $self->prospectnum;
+  $self->prospectnum('');
+  my $custnum = $self->custnum;
+  $self->custnum('');
+
+  my %link_hash = ();
+  for (qw( classnum comment selfservice_access invoice_dest )) {
+    $link_hash{$_} = $self->get($_);
+    $self->$_('');
+  }
+
+  #look for an existing contact with this email address
+  my $existing_contact = '';
+  if ( $self->get('emailaddress') =~ /\S/ ) {
+  
+    my %existing_contact = ();
+
+    foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
+      my $contact_email = qsearchs('contact_email', { emailaddress=>$email } )
+        or next;
+
+      my $contact = $contact_email->contact;
+      $existing_contact{ $contact->contactnum } = $contact;
+
+    }
+
+    if ( scalar( keys %existing_contact ) > 1 ) {
+      $dbh->rollback if $oldAutoCommit;
+      return 'Multiple email addresses specified '.
+             ' that already belong to separate contacts';
+    } elsif ( scalar( keys %existing_contact ) ) {
+      ($existing_contact) = values %existing_contact;
+    }
+
+  }
+
+  my $error;
+  if ( $existing_contact ) {
+
+    $self->$_($existing_contact->$_())
+      for qw( contactnum _password _password_encoding );
+    $error = $self->SUPER::replace($existing_contact);
+
+  } else {
+
+    $error = $self->SUPER::insert;
+
+  }
+
+  $error ||= $self->insert_password_history;
+
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
+  my $cust_contact = '';
+  # if $self->custnum was set, then the customer-specific properties
+  # (custnum, classnum, invoice_dest, selfservice_access, comment) are in
+  # pseudo-fields, and are now in %link_hash. otherwise, ignore all those
+  # fields.
+  if ( $custnum ) {
+    my %hash = ( 'contactnum' => $self->contactnum,
+                 'custnum'    => $custnum,
+               );
+    $cust_contact =  qsearchs('cust_contact', \%hash )
+                  || new FS::cust_contact { %hash, %link_hash };
+    my $error = $cust_contact->custcontactnum ? $cust_contact->replace
+                                              : $cust_contact->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  if ( $prospectnum ) {
+    my %hash = ( 'contactnum'  => $self->contactnum,
+                 'prospectnum' => $prospectnum,
+               );
+    my $prospect_contact =  qsearchs('prospect_contact', \%hash )
+                         || new FS::prospect_contact { %hash, %link_hash };
+    my $error =
+      $prospect_contact->prospectcontactnum ? $prospect_contact->replace
+                                            : $prospect_contact->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
                         keys %{ $self->hashref } ) {
     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
     my $phonetypenum = $1;
 
   foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
                         keys %{ $self->hashref } ) {
     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
     my $phonetypenum = $1;
 
-    my $contact_phone = new FS::contact_phone {
-      'contactnum' => $self->contactnum,
-      'phonetypenum' => $phonetypenum,
-      _parse_phonestring( $self->get($pf) ),
-    };
-    $error = $contact_phone->insert;
+    my %hash = ( 'contactnum'   => $self->contactnum,
+                 'phonetypenum' => $phonetypenum,
+               );
+    my $contact_phone =
+      qsearchs('contact_phone', \%hash)
+        || new FS::contact_phone { %hash, _parse_phonestring($self->get($pf)) };
+    my $error = $contact_phone->contactphonenum ? $contact_phone->replace
+                                                : $contact_phone->insert;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
@@ -149,17 +261,18 @@ sub insert {
   if ( $self->get('emailaddress') =~ /\S/ ) {
 
     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
   if ( $self->get('emailaddress') =~ /\S/ ) {
 
     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
-      my $contact_email = new FS::contact_email {
+      my %hash = (
         'contactnum'   => $self->contactnum,
         'emailaddress' => $email,
         'contactnum'   => $self->contactnum,
         'emailaddress' => $email,
-      };
-      $error = $contact_email->insert;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return $error;
+      );
+      unless ( qsearchs('contact_email', \%hash) ) {
+        my $contact_email = new FS::contact_email \%hash;
+        my $error = $contact_email->insert;
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return $error;
+        }
       }
       }
-
     }
 
   }
     }
 
   }
@@ -167,14 +280,20 @@ sub insert {
   unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
     #warn "  queueing fuzzyfiles update\n"
     #  if $DEBUG > 1;
   unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
     #warn "  queueing fuzzyfiles update\n"
     #  if $DEBUG > 1;
-    $error = $self->queue_fuzzyfiles_update;
+    my $error = $self->queue_fuzzyfiles_update;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "updating fuzzy search cache: $error";
     }
   }
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "updating fuzzy search cache: $error";
     }
   }
 
-  if ( $self->selfservice_access ) {
+  if (      $link_hash{'selfservice_access'} eq 'R'
+       or ( $link_hash{'selfservice_access'}
+            && $cust_contact
+            && ! length($self->_password)
+          )
+     )
+  {
     my $error = $self->send_reset_email( queue=>1 );
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
     my $error = $self->send_reset_email( queue=>1 );
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -208,6 +327,46 @@ sub delete {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  #got a prospetnum or custnum? delete the prospect_contact or cust_contact link
+
+  if ( $self->prospectnum ) {
+    my $prospect_contact = qsearchs('prospect_contact', {
+                             'contactnum'  => $self->contactnum,
+                             'prospectnum' => $self->prospectnum,
+                           });
+    my $error = $prospect_contact->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  # if $self->custnum was set, then we're removing the contact from this
+  # customer.
+  if ( $self->custnum ) {
+    my $cust_contact = qsearchs('cust_contact', {
+                         'contactnum'  => $self->contactnum,
+                         'custnum' => $self->custnum,
+                       });
+    my $error = $cust_contact->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  # then, proceed with deletion only if the contact isn't attached to any other
+  # prospects or customers
+
+  #inefficient, but how many prospects/customers can a single contact be
+  # attached too?  (and is removing them from one a common operation?)
+  if ( $self->prospect_contact || $self->cust_contact ) {
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    return '';
+  }
+
+  #proceed with deletion
+
   foreach my $cust_pkg ( $self->cust_pkg ) {
     $cust_pkg->contactnum('');
     my $error = $cust_pkg->replace;
   foreach my $cust_pkg ( $self->cust_pkg ) {
     $cust_pkg->contactnum('');
     my $error = $cust_pkg->replace;
@@ -225,7 +384,8 @@ sub delete {
     }
   }
 
     }
   }
 
-  my $error = $self->SUPER::delete;
+  my $error = $self->delete_password_history
+           || $self->SUPER::delete;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -262,12 +422,68 @@ sub replace {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  #save off and blank values that move to cust_contact / prospect_contact now
+  my $prospectnum = $self->prospectnum;
+  $self->prospectnum('');
+  my $custnum = $self->custnum;
+  $self->custnum('');
+
+  my %link_hash = ();
+  for (qw( classnum comment selfservice_access invoice_dest )) {
+    $link_hash{$_} = $self->get($_);
+    $self->$_('');
+  }
+
   my $error = $self->SUPER::replace($old);
   my $error = $self->SUPER::replace($old);
+  if ( $old->_password ne $self->_password ) {
+    $error ||= $self->insert_password_history;
+  }
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
+  my $cust_contact = '';
+  # if $self->custnum was set, then the customer-specific properties
+  # (custnum, classnum, invoice_dest, selfservice_access, comment) are in
+  # pseudo-fields, and are now in %link_hash. otherwise, ignore all those
+  # fields.
+  if ( $custnum ) {
+    my %hash = ( 'contactnum' => $self->contactnum,
+                 'custnum'    => $custnum,
+               );
+    my $error;
+    if ( $cust_contact = qsearchs('cust_contact', \%hash ) ) {
+      $cust_contact->$_($link_hash{$_}) for keys %link_hash;
+      $error = $cust_contact->replace;
+    } else {
+      $cust_contact = new FS::cust_contact { %hash, %link_hash };
+      $error = $cust_contact->insert;
+    }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  if ( $prospectnum ) {
+    my %hash = ( 'contactnum'  => $self->contactnum,
+                 'prospectnum' => $prospectnum,
+               );
+    my $error;
+    if ( my $prospect_contact = qsearchs('prospect_contact', \%hash ) ) {
+      $prospect_contact->$_($link_hash{$_}) for keys %link_hash;
+      $error = $prospect_contact->replace;
+    } else {
+      my $prospect_contact = new FS::prospect_contact { %hash, %link_hash };
+      $error = $prospect_contact->insert;
+    }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   foreach my $pf ( grep { /^phonetypenum(\d+)$/ }
                         keys %{ $self->hashref } ) {
     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
   foreach my $pf ( grep { /^phonetypenum(\d+)$/ }
                         keys %{ $self->hashref } ) {
     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
@@ -278,8 +494,11 @@ sub replace {
              );
     my $contact_phone = qsearchs('contact_phone', \%cp);
 
              );
     my $contact_phone = qsearchs('contact_phone', \%cp);
 
-    # if new value is empty, delete old entry
-    if (!$self->get($pf)) {
+    my $pv = $self->get($pf);
+       $pv =~ s/\s//g;
+
+    #if new value is empty, delete old entry
+    if (!$pv) {
       if ($contact_phone) {
         $error = $contact_phone->delete;
         if ( $error ) {
       if ($contact_phone) {
         $error = $contact_phone->delete;
         if ( $error ) {
@@ -292,7 +511,7 @@ sub replace {
 
     $contact_phone ||= new FS::contact_phone \%cp;
 
 
     $contact_phone ||= new FS::contact_phone \%cp;
 
-    my %cpd = _parse_phonestring( $self->get($pf) );
+    my %cpd = _parse_phonestring( $pv );
     $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
 
     my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
     $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
 
     my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
@@ -342,11 +561,14 @@ sub replace {
     }
   }
 
     }
   }
 
-  if (    ( $old->selfservice_access eq '' && $self->selfservice_access
-              && ! $self->_password
-          )
-       || $self->_resend()
-     )
+  if ( $cust_contact and (
+                              (      $cust_contact->selfservice_access eq ''
+                                  && $link_hash{selfservice_access}
+                                  && ! length($self->_password)
+                              )
+                           || $cust_contact->_resend()
+                         )
+    )
   {
     my $error = $self->send_reset_email( queue=>1 );
     if ( $error ) {
   {
     my $error = $self->send_reset_email( queue=>1 );
     if ( $error ) {
@@ -463,7 +685,6 @@ sub check {
   ;
   return $error if $error;
 
   ;
   return $error if $error;
 
-  return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
 
   return "One of first name, last name, or title must have a value"
   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
 
   return "One of first name, last name, or title must have a value"
@@ -500,23 +721,41 @@ sub firstlast {
   $self->first . ' ' . $self->last;
 }
 
   $self->first . ' ' . $self->last;
 }
 
-=item contact_classname
-
-Returns the name of this contact's class (see L<FS::contact_class>).
-
-=cut
-
-sub contact_classname {
-  my $self = shift;
-  my $contact_class = $self->contact_class or return '';
-  $contact_class->classname;
-}
+#=item contact_classname PROSPECT_OBJ | CUST_MAIN_OBJ
+#
+#Returns the name of this contact's class for the specified prospect or
+#customer (see L<FS::prospect_contact>, L<FS::cust_contact> and
+#L<FS::contact_class>).
+#
+#=cut
+#
+#sub contact_classname {
+#  my( $self, $prospect_or_cust ) = @_;
+#
+#  my $link = '';
+#  if ( ref($prospect_or_cust) eq 'FS::prospect_main' ) {
+#    $link = qsearchs('prospect_contact', {
+#              'contactnum'  => $self->contactnum,
+#              'prospectnum' => $prospect_or_cust->prospectnum,
+#            });
+#  } elsif ( ref($prospect_or_cust) eq 'FS::cust_main' ) {
+#    $link = qsearchs('cust_contact', {
+#              'contactnum'  => $self->contactnum,
+#              'custnum'     => $prospect_or_cust->custnum,
+#            });
+#  } else {
+#    croak "$prospect_or_cust is not an FS::prospect_main or FS::cust_main object";
+#  }
+#
+#  my $contact_class = $link->contact_class or return '';
+#  $contact_class->classname;
+#}
 
 =item by_selfservice_email EMAILADDRESS
 
 
 =item by_selfservice_email EMAILADDRESS
 
-Alternate search constructor (class method).  Given an email address,
-returns the contact for that address, or the empty string if no contact
-has that email address.
+Alternate search constructor (class method).  Given an email address, returns
+the contact for that address. If that contact doesn't have selfservice access,
+or there isn't one, returns the empty string.
 
 =cut
 
 
 =cut
 
@@ -527,8 +766,8 @@ sub by_selfservice_email {
     'table'     => 'contact_email',
     'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
     'hashref'   => { 'emailaddress' => $email, },
     'table'     => 'contact_email',
     'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
     'hashref'   => { 'emailaddress' => $email, },
-    'extra_sql' => " AND selfservice_access = 'Y' ".
-                   " AND ( disabled IS NULL OR disabled = '' )",
+    'extra_sql' => " AND ( contact.disabled IS NULL ) ".
+                   " AND ( contact.selfservice_access = 'Y' )",
   }) or return '';
 
   $contact_email->contact;
   }) or return '';
 
   $contact_email->contact;
@@ -567,9 +806,22 @@ sub authenticate_password {
 
 }
 
 
 }
 
+=item change_password NEW_PASSWORD
+
+Changes the contact's selfservice access password to NEW_PASSWORD. This does
+not check password policy rules (see C<is_password_allowed>) and will return
+an error only if editing the record fails for some reason.
+
+If NEW_PASSWORD is the same as the existing password, this does nothing.
+
+=cut
+
 sub change_password {
   my($self, $new_password) = @_;
 
 sub change_password {
   my($self, $new_password) = @_;
 
+  # do nothing if the password is unchanged
+  return if $self->authenticate_password($new_password);
+
   $self->change_password_fields( $new_password );
 
   $self->replace;
   $self->change_password_fields( $new_password );
 
   $self->replace;
@@ -629,13 +881,16 @@ sub send_reset_email {
 
   my $conf = new FS::Conf;
 
 
   my $conf = new FS::Conf;
 
-  my $cust_main = $self->cust_main
-    or die "no customer"; #reset a password for a prospect contact?  someday
+  my $cust_main = '';
+  my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
+  $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
 
 
-  my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum);
+  my $agentnum = $cust_main ? $cust_main->agentnum : '';
+  my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum);
   #die "selfservice-password_reset_msgnum unset" unless $msgnum;
   #die "selfservice-password_reset_msgnum unset" unless $msgnum;
-  return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
+  return "selfservice-password_reset_msgnum unset" unless $msgnum;
   my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
   my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
+  return "selfservice-password_reset_msgnum cannot be loaded" unless $msg_template;
   my %msg_template = (
     'to'            => join(',', map $_->emailaddress, @contact_email ),
     'cust_main'     => $cust_main,
   my %msg_template = (
     'to'            => join(',', map $_->emailaddress, @contact_email ),
     'cust_main'     => $cust_main,
@@ -645,11 +900,14 @@ sub send_reset_email {
 
   if ( $opt{'queue'} ) { #or should queueing just be the default?
 
 
   if ( $opt{'queue'} ) { #or should queueing just be the default?
 
+    my $cust_msg = $msg_template->prepare( %msg_template );
+    my $error = $cust_msg->insert;
+    return $error if $error;
     my $queue = new FS::queue {
     my $queue = new FS::queue {
-      'job'     => 'FS::Misc::process_send_email',
-      'custnum' => $cust_main->custnum,
+      'job'     => 'FS::cust_msg::process_send',
+      'custnum' => $cust_main ? $cust_main->custnum : '',
     };
     };
-    $queue->insert( $msg_template->prepare( %msg_template ) );
+    $queue->insert( $cust_msg->custmsgnum );
 
   } else {
 
 
   } else {
 
@@ -681,6 +939,7 @@ sub cgi_contact_fields {
 
   my @contact_fields = qw(
     classnum first last title comment emailaddress selfservice_access
 
   my @contact_fields = qw(
     classnum first last title comment emailaddress selfservice_access
+    invoice_dest
   );
 
   push @contact_fields, 'phonetypenum'. $_->phonetypenum
   );
 
   push @contact_fields, 'phonetypenum'. $_->phonetypenum
@@ -690,7 +949,57 @@ sub cgi_contact_fields {
 
 }
 
 
 }
 
-use FS::phone_type;
+use FS::upgrade_journal;
+sub _upgrade_data { #class method
+  my ($class, %opts) = @_;
+
+  # before anything else, migrate contact.custnum to cust_contact records
+  unless ( FS::upgrade_journal->is_done('contact_invoice_dest') ) {
+
+    local($skip_fuzzyfiles) = 1;
+
+    foreach my $contact (qsearch('contact', {})) {
+      my $error = $contact->replace;
+      die $error if $error;
+    }
+
+    FS::upgrade_journal->set_done('contact_invoice_dest');
+  }
+
+
+  # always migrate cust_main_invoice records over
+  local $FS::cust_main::import = 1; # override require_phone and such
+  my $search = FS::Cursor->new('cust_main_invoice', {});
+  my %custnum_dest;
+  while (my $cust_main_invoice = $search->fetch) {
+    my $custnum = $cust_main_invoice->custnum;
+    my $dest = $cust_main_invoice->dest;
+    my $cust_main = $cust_main_invoice->cust_main;
+
+    if ( $dest =~ /^\d+$/ ) {
+      my $svc_acct = FS::svc_acct->by_key($dest);
+      die "custnum $custnum, invoice destination svcnum $svc_acct does not exist\n"
+        if !$svc_acct;
+      $dest = $svc_acct->email;
+    }
+    push @{ $custnum_dest{$custnum} ||= [] }, $dest;
+
+    my $error = $cust_main_invoice->delete;
+    if ( $error ) {
+      die "custnum $custnum, cleaning up cust_main_invoice: $error\n";
+    }
+  }
+
+  foreach my $custnum (keys %custnum_dest) {
+    my $dests = $custnum_dest{$custnum};
+    my $cust_main = FS::cust_main->by_key($custnum);
+    my $error = $cust_main->replace( invoicing_list => $dests );
+    if ( $error ) {
+      die "custnum $custnum, creating contact: $error\n";
+    }
+  }
+
+}
 
 =back
 
 
 =back