don't queue fuzzyfile upgrade jobs on otaker upgrade
[freeside.git] / FS / FS / cust_main.pm
index 6d879bc..6d32e00 100644 (file)
@@ -16,6 +16,8 @@ use Exporter;
 use Scalar::Util qw( blessed );
 use List::Util qw( min );
 use Time::Local qw(timelocal);
+use Storable qw(thaw);
+use MIME::Base64;
 use Data::Dumper;
 use Tie::IxHash;
 use Digest::MD5 qw(md5_base64);
@@ -55,6 +57,7 @@ use FS::cust_tax_location;
 use FS::part_pkg_taxrate;
 use FS::agent;
 use FS::cust_main_invoice;
+use FS::cust_tag;
 use FS::cust_credit_bill;
 use FS::cust_bill_pay;
 use FS::prepay_credit;
@@ -62,6 +65,7 @@ use FS::queue;
 use FS::part_pkg;
 use FS::part_event;
 use FS::part_event_condition;
+use FS::part_export;
 #use FS::cust_event;
 use FS::type_pkgs;
 use FS::payment_gateway;
@@ -470,6 +474,30 @@ sub insert {
     $self->invoicing_list( $invoicing_list );
   }
 
+  warn "  setting customer tags\n"
+    if $DEBUG > 1;
+
+  foreach my $tagnum ( @{ $self->tagnum || [] } ) {
+    my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
+                                      'custnum' => $self->custnum };
+    my $error = $cust_tag->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  if ( $invoicing_list ) {
+    $error = $self->check_invoicing_list( $invoicing_list );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      #return "checking invoicing_list (transaction rolled back): $error";
+      return $error;
+    }
+    $self->invoicing_list( $invoicing_list );
+  }
+
+
   warn "  setting cust_main_exemption\n"
     if $DEBUG > 1;
 
@@ -546,6 +574,45 @@ sub insert {
     }
   }
 
+  # cust_main exports!
+  warn "  exporting\n" if $DEBUG > 1;
+
+  my $export_args = $options{'export_args'} || [];
+
+  my @part_export =
+    map qsearch( 'part_export', {exportnum=>$_} ),
+      $conf->config('cust_main-exports'); #, $agentnum
+
+  foreach my $part_export ( @part_export ) {
+    my $error = $part_export->export_insert($self, @$export_args);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "exporting to ". $part_export->exporttype.
+             " (transaction rolled back): $error";
+    }
+  }
+
+  #foreach my $depend_jobnum ( @$depend_jobnums ) {
+  #    warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
+  #      if $DEBUG;
+  #    foreach my $jobnum ( @jobnums ) {
+  #      my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
+  #      warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
+  #        if $DEBUG;
+  #      my $error = $queue->depend_insert($depend_jobnum);
+  #      if ( $error ) {
+  #        $dbh->rollback if $oldAutoCommit;
+  #        return "error queuing job dependancy: $error";
+  #      }
+  #    }
+  #  }
+  #
+  #}
+  #
+  #if ( exists $options{'jobnums'} ) {
+  #  push @{ $options{'jobnums'} }, @jobnums;
+  #}
+
   warn "  insert complete; committing transaction\n"
     if $DEBUG > 1;
 
@@ -1314,23 +1381,13 @@ sub delete {
     }
   }
 
-  foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
-    qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
-  ) {
-    my $error = $cust_main_invoice->delete;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
-    }
-  }
-
-  foreach my $cust_main_exemption (
-    qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } )
-  ) {
-    my $error = $cust_main_exemption->delete;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
+  foreach my $table (qw( cust_main_invoice cust_main_exemption cust_tag )) {
+    foreach my $record ( qsearch( 'table', { 'custnum' => $self->custnum } ) ) {
+      my $error = $record->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
     }
   }
 
@@ -1340,6 +1397,23 @@ sub delete {
     return $error;
   }
 
+  # cust_main exports!
+
+  #my $export_args = $options{'export_args'} || [];
+
+  my @part_export =
+    map qsearch( 'part_export', {exportnum=>$_} ),
+      $conf->config('cust_main-exports'); #, $agentnum
+
+  foreach my $part_export ( @part_export ) {
+    my $error = $part_export->export_delete( $self ); #, @$export_args);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "exporting to ". $part_export->exporttype.
+             " (transaction rolled back): $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 
@@ -1419,6 +1493,28 @@ sub replace {
     $self->invoicing_list( $invoicing_list );
   }
 
+  if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
+
+    #this could be more efficient than deleting and re-inserting, if it matters
+    foreach my $cust_tag (qsearch('cust_tag', {'custnum'=>$self->custnum} )) {
+      my $error = $cust_tag->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+    foreach my $tagnum ( @{ $self->tagnum || [] } ) {
+      my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
+                                        'custnum' => $self->custnum };
+      my $error = $cust_tag->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
   my %options = @param;
 
   my $tax_exemption = delete $options{'tax_exemption'};
@@ -1478,6 +1574,23 @@ sub replace {
     }
   }
 
+  # cust_main exports!
+
+  my $export_args = $options{'export_args'} || [];
+
+  my @part_export =
+    map qsearch( 'part_export', {exportnum=>$_} ),
+      $conf->config('cust_main-exports'); #, $agentnum
+
+  foreach my $part_export ( @part_export ) {
+    my $error = $part_export->export_replace( $self, $old, @$export_args);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "exporting to ". $part_export->exporttype.
+             " (transaction rolled back): $error";
+    }
+  }
+
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 
@@ -2107,6 +2220,9 @@ sub sort_packages {
     return 1  if !$a_num_cust_svc &&  $b_num_cust_svc;
     my @a_cust_svc = $a->cust_svc;
     my @b_cust_svc = $b->cust_svc;
+    return 0  if !scalar(@a_cust_svc) && !scalar(@b_cust_svc);
+    return -1 if  scalar(@a_cust_svc) && !scalar(@b_cust_svc);
+    return 1  if !scalar(@a_cust_svc) &&  scalar(@b_cust_svc);
     $a_cust_svc[0]->svc_x->label cmp $b_cust_svc[0]->svc_x->label;
   }
 
@@ -2385,6 +2501,42 @@ sub agent {
   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
 }
 
+=item agent_name
+
+Returns the agent name (see L<FS::agent>) for this customer.
+
+=cut
+
+sub agent_name {
+  my $self = shift;
+  $self->agent->agent;
+}
+
+=item cust_tag
+
+Returns any tags associated with this customer, as FS::cust_tag objects,
+or an empty list if there are no tags.
+
+=cut
+
+sub cust_tag {
+  my $self = shift;
+  qsearch('cust_tag', { 'custnum' => $self->custnum } );
+}
+
+=item part_tag
+
+Returns any tags associated with this customer, as FS::part_tag objects,
+or an empty list if there are no tags.
+
+=cut
+
+sub part_tag {
+  my $self = shift;
+  map $_->part_tag, $self->cust_tag; 
+}
+
+
 =item cust_class
 
 Returns the customer class, as an FS::cust_class object, or the empty string
@@ -2475,6 +2627,10 @@ Any other true value causes errors to die.
 
 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
 
+=item job
+
+Optional FS::queue entry to receive status updates.
+
 =back
 
 Options are passed to the B<bill> and B<collect> methods verbatim, so all
@@ -2491,7 +2647,9 @@ sub bill_and_collect {
   #pre-printing invoices
 
   $options{'actual_time'} ||= time;
+  my $job = $options{'job'};
 
+  $job->update_statustext('0,cleaning expired packages') if $job;
   $error = $self->cancel_expired_pkgs( $options{actual_time} );
   if ( $error ) {
     $error = "Error expiring custnum ". $self->custnum. ": $error";
@@ -2508,6 +2666,7 @@ sub bill_and_collect {
     else                                                     { warn   $error; }
   }
 
+  $job->update_statustext('20,billing packages') if $job;
   $error = $self->bill( %options );
   if ( $error ) {
     $error = "Error billing custnum ". $self->custnum. ": $error";
@@ -2516,6 +2675,7 @@ sub bill_and_collect {
     else                                                     { warn   $error; }
   }
 
+  $job->update_statustext('50,applying payments and credits') if $job;
   $error = $self->apply_payments_and_credits;
   if ( $error ) {
     $error = "Error applying custnum ". $self->custnum. ": $error";
@@ -2524,6 +2684,7 @@ sub bill_and_collect {
     else                                                     { warn   $error; }
   }
 
+  $job->update_statustext('70,running collection events') if $job;
   unless ( $conf->exists('cancelled_cust-noevents')
            && ! $self->num_ncancelled_pkgs
   ) {
@@ -2535,6 +2696,7 @@ sub bill_and_collect {
       else                                                   { warn   $error; }
     }
   }
+  $job->update_statustext('100,finished') if $job;
 
   '';
 
@@ -2684,8 +2846,14 @@ sub bill {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  warn "$me acquiring lock on customer ". $self->custnum. "\n"
+    if $DEBUG;
+
   $self->select_for_update; #mutex
 
+  warn "$me running pre-bill events for customer ". $self->custnum. "\n"
+    if $DEBUG;
+
   my $error = $self->do_cust_event(
     'debug'      => ( $options{'debug'} || 0 ),
     'time'       => $invoice_time,
@@ -2697,6 +2865,9 @@ sub bill {
     return $error;
   }
 
+  warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
+    if $DEBUG;
+
   #keep auto-charge and non-auto-charge line items separate
   my @passes = ( '', 'no_auto' );
 
@@ -3622,19 +3793,17 @@ sub collect {
     }
   }
 
-  my $error = $self->do_cust_event(
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  #never want to roll back an event just because it returned an error
+  local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+
+  $self->do_cust_event(
     'debug'      => ( $options{'debug'} || 0 ),
     'time'       => $invoice_time,
     'check_freq' => $options{'check_freq'},
     'stage'      => 'collect',
   );
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return $error;
-  }
-
-  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
-  '';
 
 }
 
@@ -3729,6 +3898,11 @@ sub do_cust_event {
     return $due_cust_event;
   }
 
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  #never want to roll back an event just because it or a different one
+  # returned an error
+  local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
+
   foreach my $cust_event ( @$due_cust_event ) {
 
     #XXX lock event
@@ -3737,11 +3911,7 @@ sub do_cust_event {
     unless ( $cust_event->test_conditions( 'time' => $time ) ) {
       #don't leave stray "new/locked" records around
       my $error = $cust_event->delete;
-      if ( $error ) {
-        #gah, even with transactions
-        $dbh->commit if $oldAutoCommit; #well.
-        return $error;
-      }
+      return $error if $error;
       next;
     }
 
@@ -3750,20 +3920,16 @@ sub do_cust_event {
       warn "  running cust_event ". $cust_event->eventnum. "\n"
         if $DEBUG > 1;
 
-      
       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
       if ( my $error = $cust_event->do_event() ) {
         #XXX wtf is this?  figure out a proper dealio with return value
         #from do_event
-         # gah, even with transactions.
-         $dbh->commit if $oldAutoCommit; #well.
-         return $error;
-       }
+        return $error;
+      }
     }
 
   }
 
-  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   '';
 
 }
@@ -7881,8 +8047,10 @@ sub email_search_result {
   my $subject = delete $params->{subject};
   my $html_body = delete $params->{html_body};
   my $text_body = delete $params->{text_body};
+  my $error = '';
 
-  my $job = delete $params->{'job'};
+  my $job = delete $params->{'job'}
+    or die "email_search_result must run from the job queue.\n";
 
   $params->{'payby'} = [ split(/\0/, $params->{'payby'}) ]
     unless ref($params->{'payby'});
@@ -7902,43 +8070,73 @@ sub email_search_result {
 
 
   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
+  my @retry_jobs = ();
+  my $success = 0;
 
   #eventually order+limit magic to reduce memory use?
   foreach my $cust_main ( qsearch($sql_query) ) {
 
+    #progressbar first, so that the count is right
+    $num++;
+    if ( time - $min_sec > $last ) {
+      my $error = $job->update_statustext(
+        int( 100 * $num / $num_cust )
+      );
+      die $error if $error;
+      $last = time;
+    }
+
     my $to = $cust_main->invoicing_list_emailonly_scalar;
-    next unless $to;
 
-    my $error = send_email(
-      generate_email(
+    if( $to ) {
+      my @message = (
         'from'      => $from,
         'to'        => $to,
         'subject'   => $subject,
         'html_body' => $html_body,
         'text_body' => $text_body,
-      )
-    );
-    return $error if $error;
+      );
 
-    if ( $job ) { #progressbar foo
-      $num++;
-      if ( time - $min_sec > $last ) {
-        my $error = $job->update_statustext(
-          int( 100 * $num / $num_cust )
-        );
-        die $error if $error;
-        $last = time;
+      $error = send_email( generate_email( @message ) );
+
+      if($error) {
+        # queue the sending of this message so that the user can see what we 
+        # tried to do, and retry if desired
+        my $queue = new FS::queue {
+          'job'        => 'FS::Misc::process_send_email',
+          'custnum'    => $cust_main->custnum,
+          'status'     => 'failed',
+          'statustext' => $error,
+        };
+        $queue->insert(@message);
+        push @retry_jobs, $queue;
       }
+      else {
+        $success++;
+      }
+    }
+
+    if($success == 0 and 
+        (scalar(@retry_jobs) > 10 or $num == $num_cust)
+      ) {
+      # 10 is arbitrary, but if we have enough failures, that's 
+      # probably a configuration or network problem, and we 
+      # abort the batch and run away screaming.
+      # We NEVER do this if anything was successfully sent.
+      $_->delete foreach (@retry_jobs);
+      return "multiple failures: '$error'\n";
     }
+  }
 
+  if(@retry_jobs) {
+    # fail the job, but with a status message that makes it clear
+    # something was sent.
+    return "Sent $success, failed ".scalar(@retry_jobs).". Failed attempts placed in job queue.\n";
   }
 
   return '';
 }
 
-use Storable qw(thaw);
-use Data::Dumper;
-use MIME::Base64;
 sub process_email_search_result {
   my $job = shift;
   #warn "$me process_re_X $method for job $job\n" if $DEBUG;
@@ -8874,6 +9072,18 @@ sub queued_bill {
   $cust_main->bill_and_collect( %args );
 }
 
+sub process_bill_and_collect {
+  my $job = shift;
+  my $param = thaw(decode_base64(shift));
+  my $cust_main = qsearchs( 'cust_main', { custnum => $param->{'custnum'} } )
+      or die "custnum '$param->{custnum}' not found!\n";
+  $param->{'job'}   = $job;
+  $param->{'fatal'} = 1; # runs from job queue, will be caught
+  $param->{'retry'} = 1;
+
+  $cust_main->bill_and_collect( %$param );
+}
+
 sub _upgrade_data { #class method
   my ($class, %opts) = @_;
 
@@ -8882,6 +9092,7 @@ sub _upgrade_data { #class method
   $sth->execute or die $sth->errstr;
 
   local($ignore_expired_card) = 1;
+  local($skip_fuzzyfiles) = 1;
   $class->_upgrade_otaker(%opts);
 
 }