ACL for hardware class config, RT#85057
[freeside.git] / FS / FS / cust_event.pm
index 355bc25..53637c5 100644 (file)
@@ -1,19 +1,19 @@
 package FS::cust_event;
+use base qw( FS::cust_main_Mixin FS::Record );
 
 use strict;
-use vars qw( @ISA $DEBUG );
+use vars qw( $DEBUG $me );
 use Carp qw( croak confess );
 use FS::Record qw( qsearch qsearchs dbdef );
-use FS::cust_main_Mixin;
-use FS::part_event;
 #for cust_X
 use FS::cust_main;
 use FS::cust_pkg;
 use FS::cust_bill;
-
-@ISA = qw(FS::cust_main_Mixin FS::Record);
+use FS::cust_pay;
+use FS::svc_acct;
 
 $DEBUG = 0;
+$me = '[FS::cust_event]';
 
 =head1 NAME
 
@@ -54,6 +54,13 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =item statustext - additional status detail (i.e. error or progress message)
 
+=item no_action - 'Y' if the event action wasn't performed. Some actions
+contain an internal check to see if the action is going to be impossible (for
+example, emailing a notice to a customer who has no email address), and if so,
+won't attempt the action. It shouldn't be reported as a failure because
+there's no need to retry it. However, the action should set no_action = 'Y'
+so that there's a record.
+
 =back
 
 =head1 METHODS
@@ -74,7 +81,7 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 sub table { 'cust_event'; }
 
-sub cust_linked { $_[0]->cust_main_custnum; } 
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum } 
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum;
@@ -139,8 +146,9 @@ sub check {
                               $dbdef_eventtable->primary_key
                             )
     || $self->ut_number('_date')
-    || $self->ut_enum('status', [qw( new locked done failed )])
+    || $self->ut_enum('status', [qw( new locked done failed initial)])
     || $self->ut_anything('statustext')
+    || $self->ut_flag('no_action')
   ;
   return $error if $error;
 
@@ -151,13 +159,6 @@ sub check {
 
 Returns the event definition (see L<FS::part_event>) for this completed event.
 
-=cut
-
-sub part_event {
-  my $self = shift;
-  qsearchs( 'part_event', { 'eventpart' => $self->eventpart } );
-}
-
 =item cust_X
 
 Returns the customer, package, invoice or batched payment (see
@@ -190,7 +191,12 @@ sub test_conditions {
   my $part_event = $self->part_event;
   my $object = $self->cust_X;
   my @conditions = $part_event->part_event_condition;
-  %opt{'cust_event'} = $self;
+  $opt{'cust_event'} = $self;
+  $opt{'time'} = $self->_date
+      or die "test_conditions called without cust_event._date\n";
+    # this MUST be set, or all hell breaks loose in event conditions.
+    # it MUST be in the same time as in the cust_event object, or
+    # future time-dependent events will trigger incorrectly.
 
   #no unsatisfied conditions
   #! grep ! $_->condition( $object, %opt ), @conditions;
@@ -213,6 +219,8 @@ Runs the event action.
 
 sub do_event {
   my $self = shift;
+  my %opt = @_; # currently only 'time'
+  my $time = $opt{'time'} || time;
 
   my $part_event = $self->part_event;
 
@@ -223,13 +231,10 @@ sub do_event {
        " (". $part_event->action. ") $for\n"
     if $DEBUG;
 
-  my $oldAutoCommit = $FS::UID::AutoCommit;
-  local $FS::UID::AutoCommit = 0;
-
   my $error;
   {
     local $SIG{__DIE__}; # don't want Mason __DIE__ handler active
-    $error = eval { $part_event->do_action($object); };
+    $error = eval { $part_event->do_action($object, $self); };
   }
 
   my $status = '';
@@ -240,13 +245,19 @@ sub do_event {
     $statustext = "Error running ". $part_event->action. " action: $@";
   } elsif ( $error ) {
     $status = 'done';
-    $statustext = $error;
+    if ( $error eq 'N/A' ) {
+      # archaic way to indicate no-op completion of spool_csv (and maybe
+      # other events)?
+      $self->no_action('Y');
+    } else {
+      $statustext = $error;
+    }
   } else {
     $status = 'done';
   }
 
   #replace or add myself
-  $self->_date(time);
+  $self->_date($time);
   $self->status($status);
   $self->statustext($statustext);
 
@@ -295,6 +306,181 @@ sub retriable {
   $self->replace($old);
 }
 
+=item join_sql
+
+=cut
+
+sub join_sql {
+  #my $class = shift;
+
+  "
+       JOIN part_event USING ( eventpart )
+
+  LEFT JOIN cust_bill ON ( eventtable = 'cust_bill' AND tablenum = cust_bill.invnum  )
+  LEFT JOIN cust_pkg  ON ( eventtable = 'cust_pkg'  AND tablenum = cust_pkg.pkgnum  )
+  LEFT JOIN cust_pay  ON ( eventtable = 'cust_pay'  AND tablenum = cust_pay.paynum  )
+  LEFT JOIN cust_pay_batch ON ( eventtable = 'cust_pay_batch' AND tablenum = cust_pay_batch.paybatchnum )
+  LEFT JOIN cust_statement ON ( eventtable = 'cust_statement' AND tablenum = cust_statement.statementnum )
+
+  LEFT JOIN cust_svc  ON ( eventtable = 'svc_acct'  AND tablenum = cust_svc.svcnum  )
+  LEFT JOIN cust_pkg AS cust_pkg_for_svc ON ( cust_svc.pkgnum = cust_pkg_for_svc.pkgnum )
+
+  LEFT JOIN cust_main ON (
+       ( eventtable = 'cust_main' AND tablenum = cust_main.custnum )
+    OR ( eventtable = 'cust_bill' AND cust_bill.custnum = cust_main.custnum )
+    OR ( eventtable = 'cust_pkg'  AND cust_pkg.custnum  = cust_main.custnum )
+    OR ( eventtable = 'cust_pay'  AND cust_pay.custnum  = cust_main.custnum )
+    OR ( eventtable = 'svc_acct'  AND cust_pkg_for_svc.custnum  = cust_main.custnum )
+  )
+  ";
+
+}
+
+=item search_sql_where HASHREF
+
+Class method which returns an SQL WHERE fragment to search for parameters
+specified in HASHREF.  Valid parameters are
+
+=over 4
+
+=item agentnum
+
+=item custnum
+
+=item invnum
+
+=item pkgnum
+
+=item svcnum
+
+=item failed
+
+=item beginning
+
+=item ending
+
+=back
+
+=cut
+
+#Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
+#sub 
+
+sub search_sql_where {
+  my($class, $param) = @_;
+  if ( $DEBUG ) {
+    warn "$me search_sql_where called with params: \n".
+         join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
+  }
+
+  my @search = $class->cust_search_sql($param);
+
+  #eventpart
+  my @eventpart = ref($param->{'eventpart'})
+                    ? @{ $param->{'eventpart'} }
+                    : split(',', $param->{'eventpart'});
+  @eventpart = grep /^(\d+)$/, @eventpart;
+  if ( @eventpart ) {
+    push @search, 'eventpart IN ('. join(',', @eventpart). ')';
+  }
+
+  if ( $param->{'beginning'} =~ /^(\d+)$/ ) {
+    push @search, "cust_event._date >= $1";
+  }
+  if ( $param->{'ending'} =~ /^(\d+)$/ ) {
+    push @search, "cust_event._date <= $1";
+  }
+
+  #if ( $param->{'failed'} ) {
+  #  push @search, "statustext != ''",
+  #                "statustext IS NOT NULL",
+  #                "statustext != 'N/A'";
+  #}
+  # huh?
+
+  my @event_status = ref($param->{'event_status'})
+                    ? @{ $param->{'event_status'} }
+                    : split(',', $param->{'event_status'});
+  if ( @event_status ) {
+    my @status;
+
+    my ($done_Y, $done_N, $done_S);
+    # done_Y: action was taken
+    # done_N: action was not taken
+    # done_S: status message returned
+    foreach (@event_status) {
+      if ($_ eq 'done_Y') {
+        $done_Y = 1;
+      } elsif ( $_ eq 'done_N' ) {
+        $done_N = 1;
+      } elsif ( $_ eq 'done_S' ) {
+        $done_S = 1;
+      } else {
+        push @status, $_;
+      }
+    }
+    if ( $done_Y or $done_N or $done_S ) {
+      push @status, 'done';
+    }
+    if ( @status ) {
+      push @search, "cust_event.status IN(" .
+                    join(',', map "'$_'", @status) .
+                    ')';
+    }
+
+    # done_S status should include only those where statustext is not null,
+    # and done_Y should include only those where it is.
+    if ( $done_Y and $done_N and $done_S ) {
+      # then not necessary
+    } else {
+      my @done_status;
+      if ( $done_Y ) {
+        push @done_status, "(cust_event.no_action IS NULL AND cust_event.statustext IS NULL)";
+      }
+      if ( $done_N ) {
+        push @done_status, "(cust_event.no_action = 'Y')";
+      }
+      if ( $done_S ) {
+        push @done_status, "(cust_event.no_action IS NULL AND cust_event.statustext IS NOT NULL)";
+      }
+      push @search, join(' OR ', @done_status) if @done_status;
+    }
+
+  } # event_status
+
+  # always hide initialization
+  push @search, 'cust_event.status != \'initial\'';
+
+  if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
+    push @search, "cust_main.custnum = '$1'";
+  }
+
+  if ( $param->{'invnum'} =~ /^(\d+)$/ ) {
+    push @search, "part_event.eventtable = 'cust_bill'",
+                  "tablenum = '$1'";
+  }
+
+  if ( $param->{'pkgnum'} =~ /^(\d+)$/ ) {
+    push @search, "part_event.eventtable = 'cust_pkg'",
+                  "tablenum = '$1'";
+  }
+
+  if ( $param->{'paynum'} =~ /^(\d+)$/ ) {
+    push @search, "part_event.eventtable = 'cust_pay'",
+                  "tablenum = '$1'";
+  }
+
+  if ( $param->{'svcnum'} =~ /^(\d+)$/ ) {
+    push @search, "part_event.eventtable = 'svc_acct'",
+                  "tablenum = '$1'";
+  }
+
+  my $where = 'WHERE '. join(' AND ', @search );
+
+  join(' AND ', @search );
+
+}
+
 =back
 
 =head1 SUBROUTINES
@@ -325,52 +511,57 @@ sub process_refax {
   process_re_X('fax', @_);
 }
 
-use Storable qw(thaw);
 use Data::Dumper;
-use MIME::Base64;
 sub process_re_X {
-  my( $method, $job ) = ( shift, shift );
-
-  my $param = thaw(decode_base64(shift));
+  my( $method, $job, $param ) = @_;
   warn Dumper($param) if $DEBUG;
 
   re_X(
     $method,
-    $param->{'beginning'},
-    $param->{'ending'},
-    $param->{'failed'},
+    $param,
     $job,
   );
 
 }
 
 sub re_X {
-  my($method, $beginning, $ending, $failed, $job) = @_;
+  my($method, $param, $job) = @_;
+
+  my $search_sql = FS::cust_event->search_sql_where($param);
 
-  my $from = 'LEFT JOIN part_event USING ( eventpart )';
+  #maybe not...?  we do want the "re-" action to match the search more closely
+  #            # yuck!  hardcoded *AND* sequential scans!
+  #my $where = " WHERE action LIKE 'cust_bill_send%' ".
+  #           ( $search_sql ? " AND $search_sql" : "" );
 
-              # yuck!  hardcoed *AND* sequential scans!
-  my $where = " WHERE action LIKE 'cust_bill_send%'".
-              "   AND cust_event._date >= $beginning".
-              "   AND cust_event._date <= $ending";
-  $where .= " AND statustext != '' AND statustext IS NOT NULL"
-    if $failed;
+  my $where = ( $search_sql ? " WHERE $search_sql" : "" );
 
   my @cust_event = qsearch({
     'table'     => 'cust_event',
-    'addl_from' => $from,
+    'addl_from' => FS::cust_event->join_sql(),
     'hashref'   => {},
     'extra_sql' => $where,
   });
 
+  warn "$me re_X found ". scalar(@cust_event). " events\n"
+    if $DEBUG;
+
   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
   foreach my $cust_event ( @cust_event ) {
 
-    # XXX 
-    $cust_event->cust_bill->$method(
-      $cust_event->part_event->templatename
-      || $cust_event->cust_main->agent_template
-    );
+    my $cust_X = $cust_event->cust_X; # cust_bill
+    next unless $cust_X->can($method);
+
+    my $part_event = $cust_event->part_event;
+    my $template = $part_event->templatename
+                   || $cust_X->agent_template;
+    my $modenum = $part_event->option('modenum') || '';
+    my $invoice_from = $part_event->option('agent_invoice_from') || '';
+    $cust_X->set('mode' => $modenum);
+    $cust_X->$method( { template => $template,
+                        modenum  => $modenum,
+                        from     => $invoice_from,
+                    } );
 
     if ( $job ) { #progressbar foo
       $num++;