#21564, external message services: preview and send messages through the UI
[freeside.git] / FS / FS / cust_main_Mixin.pm
index 8c8553c..3d05f84 100644 (file)
@@ -2,11 +2,12 @@ package FS::cust_main_Mixin;
 
 use strict;
 use vars qw( $DEBUG $me );
-use Carp qw( confess );
+use Carp qw( confess carp cluck );
 use FS::UID qw(dbh);
 use FS::cust_main;
 use FS::Record qw( qsearch qsearchs );
 use FS::Misc qw( send_email generate_email );
+use HTML::Entities;
 
 $DEBUG = 0;
 $me = '[FS::cust_main_Mixin]';
@@ -37,6 +38,7 @@ sub cust_linked { $_[0]->custnum; }
 
 sub cust_main { 
   my $self = shift;
+  cluck ref($self). '->cust_main called' if $DEBUG;
   $self->cust_linked ? qsearchs('cust_main', {custnum => $self->custnum}) : '';
 }
 
@@ -131,9 +133,12 @@ linked to a customer.
 
 sub country_full {
   my $self = shift;
-  $self->cust_linked
-    ? FS::cust_main::country_full($self)
-    : $self->cust_unlinked_msg;
+  if ( $self->locationnum ) {  # cust_pkg has this
+    my $location = FS::cust_location->by_key($self->locationnum);
+    $location ? $location->country_full : '';
+  } elsif ( $self->cust_linked ) {
+    $self->cust_main->bill_country_full;
+  }
 }
 
 =item invoicing_list_emailonly
@@ -230,12 +235,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
@@ -388,6 +407,7 @@ in the job fail, the entire job will abort and return an error.
 use Storable qw(thaw);
 use MIME::Base64;
 use Data::Dumper qw(Dumper);
+use Digest::SHA qw(sha1); # for duplicate checking
 
 sub email_search_result {
   my($class, $param) = @_;
@@ -408,10 +428,8 @@ sub email_search_result {
       or die "msgnum $msgnum not found\n";
   }
 
-  $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
-    unless ref($param->{'payby'});
-
   my $sql_query = $class->search($param->{'search'});
+  $sql_query->{'select'} = $sql_query->{'table'} . '.*';
 
   my $count_query   = delete($sql_query->{'count_query'});
   my $count_sth = dbh->prepare($count_query)
@@ -427,6 +445,10 @@ sub email_search_result {
   my $success = 0;
   my %sent_to = ();
 
+  if ( !$msg_template ) {
+    # XXX create on the fly
+  }
+
   #eventually order+limit magic to reduce memory use?
   foreach my $obj ( qsearch($sql_query) ) {
 
@@ -441,52 +463,41 @@ sub email_search_result {
     }
 
     my $cust_main = $obj->cust_main;
-    my @message;
     if ( !$cust_main ) { 
       next; # unlinked object; nothing else we can do
     }
 
-    if( $sent_to{$cust_main->custnum} ) {
+    my $cust_msg = $msg_template->prepare(
+      'cust_main' => $cust_main,
+      'object'    => $obj,
+    );
+
+    # For non-cust_main searches, we avoid duplicates based on message
+    # body text.
+    my $unique = $cust_main->custnum;
+    $unique .= sha1($cust_msg->text_body) if $class ne 'FS::cust_main';
+    if( $sent_to{$unique} ) {
       # avoid duplicates
       $dups++;
       next;
     }
 
-    $sent_to{$cust_main->custnum} = 1;
+    $sent_to{$unique} = 1;
     
-    if ( $msg_template ) {
-      # XXX add support for other context objects?
-      # If we do that, handling of "duplicates" will 
-      # have to be smarter.  Currently we limit to 
-      # one message per custnum because they'd all
-      # be identical.
-      @message = $msg_template->prepare( 'cust_main' => $cust_main );
-    }
-    else {
-      my $to = $cust_main->invoicing_list_emailonly_scalar;
-      next if !$to;
-
-      @message = (
-        'from'      => $from,
-        'to'        => $to,
-        'subject'   => $subject,
-        'html_body' => $html_body,
-        'text_body' => $text_body,
-      );
-    } #if $msg_template
-
-    $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 {
@@ -518,7 +529,7 @@ sub process_email_search_result {
   my $job = shift;
   #warn "$me process_re_X $method for job $job\n" if $DEBUG;
 
-  my $param = thaw(decode_base64(shift));
+  my $param = shift;
   warn Dumper($param) if $DEBUG;
 
   $param->{'job'} = $job;
@@ -536,10 +547,107 @@ sub process_email_search_result {
   die "error loading FS::$table: $@\n" if $@;
 
   my $error = "FS::$table"->email_search_result( $param );
+  dbh->commit; # save failed jobs before rethrowing the error
   die $error if $error;
 
 }
 
+=item conf
+
+Returns a configuration handle (L<FS::Conf>) set to the customer's locale, 
+if they have one.  If not, returns an FS::Conf with no locale.
+
+=cut
+
+sub conf {
+  my $self = shift;
+  return $self->{_conf} if (ref $self and $self->{_conf});
+  my $cust_main = $self->cust_main;
+  my $conf = new FS::Conf { 
+    'locale' => ($cust_main ? $cust_main->locale : '')
+  };
+  $self->{_conf} = $conf if ref $self;
+  return $conf;
+}
+
+=item mt TEXT [, ARGS ]
+
+Localizes a text string (see L<Locale::Maketext>) for the customer's locale,
+if they have one.
+
+=cut
+
+sub mt {
+  my $self = shift;
+  return $self->{_lh}->maketext(@_) if (ref $self and $self->{_lh});
+  my $cust_main = $self->cust_main;
+  my $locale = $cust_main ? $cust_main->locale : '';
+  my $lh = FS::L10N->get_handle($locale);
+  $self->{_lh} = $lh if ref $self;
+  return $lh->maketext(@_);
+}
+
+=item time2str_local FORMAT, TIME[, ESCAPE]
+
+Localizes a date (see L<Date::Language>) for the customer's locale.
+
+FORMAT can be a L<Date::Format> string, or one of these special words:
+
+- "short": the value of the "date_format" config setting for the customer's 
+  locale, defaulting to "%x".
+- "rdate": the same as "short" except that the default has a four-digit year.
+- "long": the value of the "date_format_long" config setting for the 
+  customer's locale, defaulting to "%b %o, %Y".
+
+ESCAPE, if specified, is one of "latex" or "html", and will escape non-ASCII
+characters and convert spaces to nonbreaking spaces.
+
+=cut
+
+sub time2str_local {
+  # renamed so that we don't have to change every single reference to 
+  # time2str everywhere
+  my $self = shift;
+  my ($format, $time, $escape) = @_;
+  return '' unless $time > 0; # work around time2str's traditional stupidity
+
+  $self->{_date_format} ||= {};
+  if (!exists($self->{_dh})) {
+    my $cust_main = $self->cust_main;
+    my $locale = $cust_main->locale  if $cust_main;
+    $locale ||= 'en_US';
+    my %info = FS::Locales->locale_info($locale);
+    my $dh = eval { Date::Language->new($info{'name'}) } ||
+             Date::Language->new(); # fall back to English
+    $self->{_dh} = $dh;
+  }
+
+  if ($format eq 'short') {
+    $format = $self->{_date_format}->{short}
+            ||= $self->conf->config('date_format') || '%x';
+  } elsif ($format eq 'rdate') {
+    $format = $self->{_date_format}->{rdate}
+            ||= $self->conf->config('date_format') || '%m/%d/%Y';
+  } elsif ($format eq 'long') {
+    $format = $self->{_date_format}->{long}
+            ||= $self->conf->config('date_format_long') || '%b %o, %Y';
+  }
+
+  # actually render the date
+  my $string = $self->{_dh}->time2str($format, $time);
+
+  if ($escape) {
+    if ($escape eq 'html') {
+      $string = encode_entities($string);
+      $string =~ s/ +/&nbsp;/g;
+    } elsif ($escape eq 'latex') { # just do nbsp's here
+      $string =~ s/ +/~/g;
+    }
+  }
+  
+  $string;
+}
+
 =back
 
 =head1 BUGS