add option to limit automatic unsuspensions to a specific suspension reason type...
[freeside.git] / FS / FS / cust_main_Mixin.pm
index 4b151e7..6a2e905 100644 (file)
@@ -210,19 +210,9 @@ a customer.
 sub cust_status {
   my $self = shift;
   return $self->cust_unlinked_msg unless $self->cust_linked;
-
-  #FS::cust_main::status($self)
-  #false laziness w/actual cust_main::status
-  # (make sure FS::cust_main methods are called)
-  for my $status (qw( prospect active inactive suspended cancelled )) {
-    my $method = $status.'_sql';
-    my $sql = FS::cust_main->$method();;
-    my $numnum = ( $sql =~ s/cust_main\.custnum/?/g );
-    my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
-    $sth->execute( ($self->custnum) x $numnum )
-      or die "Error executing 'SELECT $sql': ". $sth->errstr;
-    return $status if $sth->fetchrow_arrayref->[0];
-  }
+  my $cust_main = $self->cust_main;
+  return $self->cust_unlinked_msg unless $cust_main;
+  return $cust_main->cust_status;
 }
 
 =item ucfirst_cust_status
@@ -235,12 +225,26 @@ linked to a customer.
 =cut
 
 sub ucfirst_cust_status {
+  carp "ucfirst_cust_status deprecated, use cust_status_label";
+  local($FS::cust_main::ucfirst_nowarn) = 1;
   my $self = shift;
   $self->cust_linked
     ? ucfirst( $self->cust_status(@_) ) 
     : $self->cust_unlinked_msg;
 }
 
+=item cust_status_label
+
+=cut
+
+sub cust_status_label {
+  my $self = shift;
+
+  $self->cust_linked
+    ? FS::cust_main::cust_status_label($self)
+    : $self->cust_unlinked_msg;
+}
+
 =item cust_statuscolor
 
 Given an object that contains fields from cust_main (say, from a JOINed
@@ -258,6 +262,17 @@ sub cust_statuscolor {
     : '000000';
 }
 
+=item agent_name
+
+=cut
+
+sub agent_name {
+  my $self = shift;
+  $self->cust_linked
+    ? $self->cust_main->agent_name
+    : $self->cust_unlinked_msg;
+}
+
 =item prospect_sql
 
 =item active_sql
@@ -299,8 +314,6 @@ in HASHREF.  Valid parameters are:
 
 =item status
 
-=item payby
-
 =back
 
 =cut
@@ -325,15 +338,6 @@ sub cust_search_sql {
     push @search, $class->$method();
   }
 
-  #payby
-  my @payby = ref($param->{'payby'})
-                ? @{ $param->{'payby'} }
-                : split(',', $param->{'payby'});
-  @payby = grep /^([A-Z]{4})$/, @payby;
-  if ( @payby ) {
-    push @search, 'cust_main.payby IN ('. join(',', map "'$_'", @payby). ')';
-  }
-
   #here is the agent virtualization
   push @search,
     $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' );
@@ -344,10 +348,21 @@ sub cust_search_sql {
 
 =item email_search_result HASHREF
 
-Emails a notice to the specified customers.  Customers without 
-invoice email destinations will be skipped.
+Emails a notice to the specified customer's contact_email addresses.
+
+
+If the user has specified "Invoice recipients" on the send e-mail screen,
+contact_email rows containing the invoice_dest flag will be included.
+This option is default, if neither 'invoice' nor 'message' are present.
+
+If the user has specified "Message recipients" on the send e-mail screen,
+contact_email rows containing the message_dest flag will be included.
 
-Parameters: 
+The selection is indicated by the presence of the text 'message' or
+'invoice' within the to_contact_classnum argument.
+
+
+Parameters:
 
 =over 4
 
@@ -380,6 +395,22 @@ HTML body
 
 Text body
 
+=item to_contact_classnum
+
+This field contains a comma-separated list.  This list may contain:
+
+- the text "invoice" indicating contacts with invoice_dest flag should
+  be included
+- the text "message" indicating contacts with message_dest flag should
+  be included
+- numbers representing classnum id values for email contact classes.
+  If any classnum are present, emails should only be sent to contact_email
+  addresses where contact_email.classnum contains one of these classes.
+  The classnum 0 also includes where contact_email.classnum IS NULL
+
+If neither 'invoice' nor 'message' has been specified, this method will
+behave as if 'invoice' had been selected
+
 =back
 
 Returns an error message, or false for success.
@@ -398,13 +429,21 @@ use Digest::SHA qw(sha1); # for duplicate checking
 sub email_search_result {
   my($class, $param) = @_;
 
+  my $conf = FS::Conf->new;
+  my $send_to_domain = $conf->config('email-to-voice_domain');
+
   my $msgnum = $param->{msgnum};
   my $from = delete $param->{from};
   my $subject = delete $param->{subject};
   my $html_body = delete $param->{html_body};
   my $text_body = delete $param->{text_body};
+  my $to_contact_classnum = delete $param->{to_contact_classnum};
+  my $emailtovoice_name = delete $param->{emailtovoice_contact};
+
   my $error = '';
 
+  my $to = $emailtovoice_name . '@' . $send_to_domain unless !$emailtovoice_name;
+
   my $job = delete $param->{'job'}
     or die "email_search_result must run from the job queue.\n";
   
@@ -412,6 +451,18 @@ sub email_search_result {
   if ( $msgnum ) {
     $msg_template = qsearchs('msg_template', { msgnum => $msgnum } )
       or die "msgnum $msgnum not found\n";
+  } else {
+    $msg_template = FS::msg_template->new({
+        from_addr => $from,
+        msgname   => $subject, # maybe a timestamp also?
+        disabled  => 'D', # 'D'raft
+        # msgclass, maybe
+    });
+    $error = $msg_template->insert(
+      subject => $subject,
+      body    => $html_body,
+    );
+    return "$error (when creating draft template)" if $error;
   }
 
   my $sql_query = $class->search($param->{'search'});
@@ -431,6 +482,10 @@ sub email_search_result {
   my $success = 0;
   my %sent_to = ();
 
+  if ( !$msg_template ) {
+    die "email_search_result now requires a msg_template";
+  }
+
   #eventually order+limit magic to reduce memory use?
   foreach my $obj ( qsearch($sql_query) ) {
 
@@ -445,36 +500,24 @@ sub email_search_result {
     }
 
     my $cust_main = $obj->cust_main;
-    tie my %message, 'Tie::IxHash';
     if ( !$cust_main ) { 
       next; # unlinked object; nothing else we can do
     }
 
-    if ( $msg_template ) {
-      # Now supports other context objects.
-      %message = $msg_template->prepare(
-        'cust_main' => $cust_main,
-        'object'    => $obj,
-      );
-    }
-    else {
-      my @to = $cust_main->invoicing_list_emailonly;
-      next if !@to;
-
-      %message = (
-        'from'      => $from,
-        'to'        => \@to,
-        'subject'   => $subject,
-        'html_body' => $html_body,
-        'text_body' => $text_body,
-        'custnum'   => $cust_main->custnum,
-      );
-    } #if $msg_template
+    my %to = ();
+    if ($to) { $to{'to'} = $to; }
+
+    my $cust_msg = $msg_template->prepare(
+      'cust_main' => $cust_main,
+      'object'    => $obj,
+      'to_contact_classnum' => $to_contact_classnum,
+      %to,
+    );
 
     # For non-cust_main searches, we avoid duplicates based on message
-    # body text.  
+    # body text.
     my $unique = $cust_main->custnum;
-    $unique .= sha1($message{'text_body'}) if $class ne 'FS::cust_main';
+    $unique .= sha1($cust_msg->text_body) if $class ne 'FS::cust_main';
     if( $sent_to{$unique} ) {
       # avoid duplicates
       $dups++;
@@ -483,18 +526,20 @@ sub email_search_result {
 
     $sent_to{$unique} = 1;
     
-    $error = send_email( generate_email( %message ) );
+    $error = $cust_msg->send;
 
     if($error) {
       # queue the sending of this message so that the user can see what we
       # tried to do, and retry if desired
+      # (note the cust_msg itself also now has a status of 'failed'; that's 
+      # fine, as it will get its status reset if we retry the job)
       my $queue = new FS::queue {
-        'job'        => 'FS::Misc::process_send_email',
+        'job'        => 'FS::cust_msg::process_send',
         'custnum'    => $cust_main->custnum,
         'status'     => 'failed',
         'statustext' => $error,
       };
-      $queue->insert(%message);
+      $queue->insert($cust_msg->custmsgnum);
       push @retry_jobs, $queue;
     }
     else {
@@ -513,6 +558,14 @@ sub email_search_result {
     }
   } # foreach $obj
 
+  # if the message template was created as "draft", change its status to
+  # "completed"
+  if ($msg_template->disabled eq 'D') {
+    $msg_template->set('disabled' => 'C');
+    my $error = $msg_template->replace;
+    warn "$error (setting draft message template status)" if $error;
+  }
+
   if(@retry_jobs) {
     # fail the job, but with a status message that makes it clear
     # something was sent.
@@ -534,9 +587,6 @@ sub process_email_search_result {
   $param->{'search'} = thaw(decode_base64($param->{'search'}))
     or die "process_email_search_result requires search params.\n";
 
-#  $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
-#    unless ref($param->{'payby'});
-
   my $table = $param->{'table'} 
     or die "process_email_search_result requires table.\n";
 
@@ -645,6 +695,59 @@ sub time2str_local {
   $string;
 }
 
+=item unsuspend_balance
+
+If conf I<unsuspend_balance> is set and customer's current balance is
+beneath the set threshold, unsuspends customer packages.
+
+=cut
+
+sub unsuspend_balance {
+  my $self = shift;
+  my $cust_main = $self->cust_main;
+  my $conf = $self->conf;
+  my $setting = $conf->config('unsuspend_balance') or return;
+  my $maxbalance;
+  if ($setting eq 'Zero') {
+    $maxbalance = 0;
+
+  # kind of a pain to load/check all cust_bill instead of just open ones,
+  # but if for some reason payment gets applied to later bills before
+  # earlier ones, we still want to consider the later ones as allowable balance
+  } elsif ($setting eq 'Latest invoice charges') {
+    my @cust_bill = $cust_main->cust_bill();
+    my $cust_bill = $cust_bill[-1]; #always want the most recent one
+    if ($cust_bill) {
+      $maxbalance = $cust_bill->charged || 0;
+    } else {
+      $maxbalance = 0;
+    }
+  } elsif ($setting eq 'Charges not past due') {
+    my $now = time;
+    $maxbalance = 0;
+    foreach my $cust_bill ($cust_main->cust_bill()) {
+      next unless $now <= ($cust_bill->due_date || $cust_bill->_date);
+      $maxbalance += $cust_bill->charged || 0;
+    }
+  } elsif (length($setting)) {
+    warn "Unrecognized unsuspend_balance setting $setting";
+    return;
+  } else {
+    return;
+  }
+  my $balance = $cust_main->balance || 0;
+  if ($balance <= $maxbalance) {
+    my @errors = $cust_main->unsuspend(
+                  'reason_type' => $conf->config('unsuspend_reason_type')
+                );
+    # side-fx with nested transactions?  upstack rolls back?
+    warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
+         join(' / ', @errors)
+      if @errors;
+  }
+  return;
+}
+
 =back
 
 =head1 BUGS
@@ -656,4 +759,3 @@ L<FS::cust_main>, L<FS::Record>
 =cut
 
 1;
-