when canceling services across multiple packages, transaction-protect each one separa...
[freeside.git] / FS / FS / msg_template.pm
index 644663e..13c8d9c 100644 (file)
@@ -239,7 +239,7 @@ Options are passed as a list of name/value pairs:
 
 =item cust_main
 
-Customer object (required).
+Customer object (optional)
 
 =item object
 
@@ -268,19 +268,7 @@ invoicing_list addresses.  Multiple addresses may be comma-separated.
 
 =item substitutions
 
-A hash reference of additional string substitutions
-
-=item sub_param
-
-A hash reference, keys are the names of existing substitutions,
-values are an addition parameter object to pass to the subroutine
-for that substitution, e.g.
-
-       'sub_param' => {
-         'payment_history' => {
-           'start_date' => 1434764295,
-         },
-       },
+A hash reference of additional substitutions
 
 =back
 
@@ -289,16 +277,21 @@ for that substitution, e.g.
 sub prepare {
   my( $self, %opt ) = @_;
 
-  my $cust_main = $opt{'cust_main'} or die 'cust_main required';
-  my $object = $opt{'object'} or die 'object required';
+  my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
+  my $object = $opt{'object'}; # or die 'object required';
+
+  my $locale = $cust_main ? $cust_main->locale : '';
 
-  # localization
-  my $locale = $cust_main->locale || '';
   warn "no locale for cust#".$cust_main->custnum."; using default content\n"
-    if $DEBUG and !$locale;
-  my $content = $self->content($cust_main->locale);
-  warn "preparing template '".$self->msgname."' to cust#".$cust_main->custnum."\n"
-    if($DEBUG);
+    if $DEBUG and $cust_main and !$locale;
+
+  my $content = $self->content( $locale );
+
+  warn sprintf(
+    "preparing template '%s' to cust#%s\n",
+    $self->msgname,
+    $cust_main ? $cust_main->custnum : 'none'
+  ) if $DEBUG;
 
   my $subs = $self->substitutions;
 
@@ -306,8 +299,11 @@ sub prepare {
   # create substitution table
   ###  
   my %hash;
-  my @objects = ($cust_main);
-  my @prefixes = ('');
+  my ( @objects, @prefixes );
+  if ( $cust_main ) {
+    @objects  = ( $cust_main );
+    @prefixes = ( '' );
+  }
   my $svc;
   if( ref $object ) {
     if( ref($object) eq 'ARRAY' ) {
@@ -336,10 +332,7 @@ sub prepare {
       }
       elsif( ref($name) eq 'ARRAY' ) {
         # [ foo => sub { ... } ]
-        my @subparam = ();
-        push(@subparam, $opt{'sub_param'}->{$name->[0]})
-          if $opt{'sub_param'} && $opt{'sub_param'}->{$name->[0]};
-        $hash{$prefix.($name->[0])} = $name->[1]->($obj,@subparam);
+        $hash{$prefix.($name->[0])} = $name->[1]->($obj);
       }
       else {
         warn "bad msg_template substitution: '$name'\n";
@@ -352,10 +345,7 @@ sub prepare {
     $hash{$_} = $opt{substitutions}->{$_} foreach keys %{$opt{substitutions}};
   }
 
-  foreach my $key (keys %hash) {
-    next if $self->no_encode($key);
-    $hash{$key} = encode_entities($_ || '');
-  };
+  $_ = encode_entities($_ || '') foreach values(%hash);
 
   ###
   # clean up template
@@ -401,21 +391,35 @@ sub prepare {
 
   my @to;
   if ( exists($opt{'to'}) ) {
+
     @to = split(/\s*,\s*/, $opt{'to'});
+
+  } elsif ( $cust_main ) {
+
+    my @classes;
+    if ( $opt{'to_contact_classnum'} ) {
+      my $classnum = $opt{'to_contact_classnum'};
+      @classes = ref($classnum) ? @$classnum : split(',', $classnum);
+    }
+    if (!@classes) {
+      @classes = ( 'invoice' );
+    }
+    @to = $cust_main->contact_list_email(@classes);
+
+  } else {
+
+    die 'no To: address or cust_main object specified';
+
   }
-  else {
-    @to = $cust_main->invoicing_list_emailonly;
-  }
-  # no warning when preparing with no destination
 
   my $from_addr = $self->from_addr;
 
   if ( !$from_addr ) {
+    my @agentnum = ( $cust_main->agentnum ) if $cust_main;
     if ( $opt{'from_config'} ) {
-      $from_addr = scalar( $conf->config($opt{'from_config'}, 
-                                         $cust_main->agentnum) );
+      $from_addr = scalar( $conf->config( $opt{'from_config'}, @agentnum ));
     }
-    $from_addr ||= $conf->invoice_from_full($cust_main->agentnum);
+    $from_addr ||= $conf->invoice_from_full( @agentnum );
   }
 #  my @cust_msg = ();
 #  if ( $conf->exists('log_sent_mail') and !$opt{'preview'} ) {
@@ -433,14 +437,14 @@ sub prepare {
                       ->format( HTML::TreeBuilder->new_from_content($body) )
                   );
   (
-    'custnum' => $cust_main->custnum,
-    'msgnum'  => $self->msgnum,
-    'from' => $from_addr,
-    'to'   => \@to,
-    'bcc'  => $self->bcc_addr || undef,
+    'custnum'   => $cust_main ? $cust_main->custnum : undef,
+    'msgnum'    => $self->msgnum,
+    'from'      => $from_addr,
+    'to'        => \@to,
+    'bcc'       => $self->bcc_addr || undef,
     'subject'   => $subject,
     'html_body' => $body,
-    'text_body' => $text_body
+    'text_body' => $text_body,
   );
 
 }
@@ -448,7 +452,8 @@ sub prepare {
 =item send OPTION => VALUE
 
 Fills in the template and sends it to the customer.  Options are as for 
-'prepare'.
+'prepare', plus 'attach', a L<MIME::Entity> (or arrayref of them) to attach
+to the message.
 
 =cut
 
@@ -456,7 +461,20 @@ Fills in the template and sends it to the customer.  Options are as for
 # preview it, etc.
 sub send {
   my $self = shift;
-  send_email(generate_email($self->prepare(@_)));
+  my %opt = @_;
+
+  my %email = generate_email($self->prepare(%opt));
+  if ( $opt{'attach'} ) {
+    my @attach;
+    if (ref($opt{'attach'}) eq 'ARRAY') {
+      @attach = @{ $opt{'attach'} };
+    } else {
+      @attach = $opt{'attach'};
+    }
+    push @{ $email{mimeparts} }, @attach;
+  }
+
+  send_email(%email);
 }
 
 =item render OPTION => VALUE ...
@@ -522,17 +540,21 @@ my $usage_warning = sub {
 
 #my $conf = new FS::Conf;
 
-# for substitutions that handle their own encoding
-sub no_encode {
-  my $self = shift;
-  my $field = shift;
-  return ($field eq 'payment_history');
-}
-
 #return contexts and fill-in values
 # If you add anything, be sure to add a description in 
 # httemplate/edit/msg_template.html.
 sub substitutions {
+  my $payinfo_sub = sub { 
+    my $obj = shift;
+    ($obj->payby eq 'CARD' || $obj->payby eq 'CHEK')
+    ? $obj->paymask 
+    : $obj->decrypt($obj->payinfo)
+  };
+  my $payinfo_end = sub {
+    my $obj = shift;
+    my $payinfo = &$payinfo_sub($obj);
+    substr($payinfo, -4);
+  };
   { 'cust_main' => [qw(
       display_custnum agentnum agent_name
 
@@ -587,12 +609,6 @@ sub substitutions {
       [ selfservice_server_base_url => sub { 
           $conf->config('selfservice_server-base_url') #, shift->agentnum) 
         } ],
-      [ payment_history => sub {
-          my $cust_main = shift;
-          my $param = shift || {};
-          #html works, see no_encode method
-          return '<PRE>' . encode_entities($cust_main->payment_history_text($param)) . '</PRE>';
-        } ],
     ],
     # next_bill_date
     'cust_pkg'  => [qw( 
@@ -685,11 +701,17 @@ sub substitutions {
       # overrides the one in cust_main in cases where a cust_pay is passed
       [ payby             => sub { FS::payby->shortname(shift->payby) } ],
       [ date              => sub { time2str("%a %B %o, %Y", shift->_date) } ],
-      [ payinfo           => sub { 
-          my $cust_pay = shift;
-          ($cust_pay->payby eq 'CARD' || $cust_pay->payby eq 'CHEK') ?
-            $cust_pay->paymask : $cust_pay->decrypt($cust_pay->payinfo)
-        } ],
+      [ 'payinfo' => $payinfo_sub ],
+      [ 'payinfo_end' => $payinfo_end ],
+    ],
+    # for refund receipts
+    'cust_refund' => [
+      'refundnum',
+      [ refund            => sub { sprintf("%.2f", shift->refund) } ],
+      [ payby             => sub { FS::payby->shortname(shift->payby) } ],
+      [ date              => sub { time2str("%a %B %o, %Y", shift->_date) } ],
+      [ 'payinfo' => $payinfo_sub ],
+      [ 'payinfo_end' => $payinfo_end ],
     ],
     # for payment decline messages
     # try to support all cust_pay fields
@@ -701,11 +723,8 @@ sub substitutions {
       [ paid              => sub { sprintf("%.2f", shift->paid) } ],
       [ payby             => sub { FS::payby->shortname(shift->payby) } ],
       [ date              => sub { time2str("%a %B %o, %Y", shift->_date) } ],
-      [ payinfo           => sub {
-          my $pending = shift;
-          ($pending->payby eq 'CARD' || $pending->payby eq 'CHEK') ?
-            $pending->paymask : $pending->decrypt($pending->payinfo)
-        } ],
+      [ 'payinfo' => $payinfo_sub ],
+      [ 'payinfo_end' => $payinfo_end ],
     ],
   };
 }
@@ -868,6 +887,25 @@ sub _upgrade_data {
   ###
   $self->_populate_initial_data;
 
+  ### Fix dump-email_to (needs to happen after _populate_initial_data)
+  if ($conf->config('dump-email_to')) {
+    # anyone who still uses dump-email_to should have just had this created
+    my ($msg_template) = qsearch('msg_template',{ msgname => 'System log' });
+    if ($msg_template) {
+      eval "use FS::log_email;";
+      die $@ if $@;
+      my $log_email = new FS::log_email {
+        'context' => 'Cron::backup',
+        'min_level' => 1,
+        'msgnum' => $msg_template->msgnum,
+        'to_addr' => $conf->config('dump-email_to'),
+      };
+      my $error = $log_email->insert;
+      die $error if $error;
+      $conf->delete('dump-email_to');
+    }
+  }
+
 }
 
 sub _populate_initial_data { #class method
@@ -876,18 +914,22 @@ sub _populate_initial_data { #class method
 
   eval "use FS::msg_template::InitialData;";
   die $@ if $@;
+  eval "use FS::upgrade_journal;";
+  die $@ if $@;
 
   my $initial_data = FS::msg_template::InitialData->_initial_data;
 
   foreach my $hash ( @$initial_data ) {
 
     next if $hash->{_conf} && $conf->config( $hash->{_conf} );
+    next if $hash->{_upgrade_journal} && FS::upgrade_journal->is_done( $hash->{_upgrade_journal} );
 
     my $msg_template = new FS::msg_template($hash);
     my $error = $msg_template->insert( @{ $hash->{_insert_args} || [] } );
     die $error if $error;
 
     $conf->set( $hash->{_conf}, $msg_template->msgnum ) if $hash->{_conf};
+    FS::upgrade_journal->set_done( $hash->{_upgrade_journal} );
   
   }