better error handling when a package change fails, RT#78504
[freeside.git] / FS / FS / Cron / bill.pm
index 62bb321..30eb1ab 100644 (file)
@@ -5,13 +5,16 @@ use vars qw( @ISA @EXPORT_OK );
 use Exporter;
 use Date::Parse;
 use DBI 1.33; #The "clone" method was added in DBI 1.33. 
-use FS::UID qw(dbh);
+use FS::UID qw( dbh driver_name );
 use FS::Record qw( qsearch qsearchs );
+use FS::Misc::DateTime qw( day_end );
 use FS::queue;
 use FS::cust_main;
 use FS::part_event;
 use FS::part_event_condition;
 
+use FS::Log;
+
 @ISA = qw( Exporter );
 @EXPORT_OK = qw ( bill bill_where );
 
@@ -26,6 +29,9 @@ use FS::part_event_condition;
 sub bill {
   my %opt = @_;
 
+  my $log = FS::Log->new('Cron::bill');
+  $log->info('start');
+
   my $check_freq = $opt{'check_freq'} || '1d';
 
   my $debug = 0;
@@ -35,9 +41,10 @@ sub bill {
   #$FS::cust_event::DEBUG = $opt{'l'} if $opt{'l'};
 
   my $conf = new FS::Conf;
+  my $disable_bill = 0;
   if ( $conf->exists('disable_cron_billing') ) {
     warn "disable_cron_billing set, skipping billing\n" if $debug;
-    return;
+    $disable_bill = 1;
   }
 
   #we're at now now (and later).
@@ -56,14 +63,20 @@ sub bill {
 
   my $cursor_dbh = dbh->clone;
 
-  $cursor_dbh->do(
-    "DECLARE cron_bill_cursor CURSOR FOR ".
-    "  SELECT custnum FROM cust_main WHERE ". bill_where( %opt )
-  ) or die $cursor_dbh->errstr;
+  my $select = 'SELECT custnum FROM cust_main WHERE '. bill_where( %opt );
+
+  unless ( driver_name =~ /^mysql/ ) {
+    $cursor_dbh->do( "DECLARE cron_bill_cursor CURSOR FOR $select" )
+      or die $cursor_dbh->errstr;
+  }
 
   while ( 1 ) {
 
-    my $sth = $cursor_dbh->prepare('FETCH 100 FROM cron_bill_cursor'); #mysql?
+    my $sql = (driver_name =~ /^mysql/)
+      ? $select
+      : 'FETCH 100 FROM cron_bill_cursor';
+
+    my $sth = $cursor_dbh->prepare($sql);
 
     $sth->execute or die $sth->errstr;
 
@@ -86,6 +99,8 @@ sub bill {
           'check_freq'   => $check_freq,
           'resetup'      => ( $opt{'s'} ? $opt{'s'} : 0 ),
           'not_pkgpart'  => $opt{'g'}, #$not_pkgpart,
+          'one_recur'    => $opt{'o'},
+          'no_prepaid'   => 1,
       );
 
       if ( $opt{'m'} ) {
@@ -108,22 +123,34 @@ sub bill {
             'priority' => 99, #don't get in the way of provisioning jobs
           };
           my $error = $queue->insert( 'custnum'=>$custnum, %args );
-
+          die $error if $error;
         }
 
       } else {
 
         my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } );
-        $cust_main->bill_and_collect( %args, 'debug' => $debug );
+        if ( $disable_bill ) {
+          $cust_main->collect( %args, 'debug' => $debug );
+        } else {
+          my $error = $cust_main->bill_and_collect( %args, 'fatal' => 'return',
+                                                           'debug' => $debug, );
+          if ( $error ) {
+            $log->error($error);
+            warn $error; #die $error;
+          }
+        }
 
       }
 
     }
 
+    last if driver_name =~ /^mysql/;
+
   }
 
   $cursor_dbh->commit or die $cursor_dbh->errstr;
 
+  $log->info('finish');
 }
 
 # freeside-daily %opt:
@@ -136,17 +163,24 @@ sub bill {
 #      (or now, if no -d switch was given).
 #
 #  -n: When used with "-d" and/or "-y", specifies that invoices should be dated
-#      with today's date, irregardless of the pretend date used to pre-generate
+#      with today's date, regardless of the pretend date used to pre-generate
 #      the invoices.
 #
-#  -p: Only process customers with the specified payby (I<CARD>, I<DCRD>, I<CHEK>, I<DCHK>, I<BILL>, I<COMP>, I<LECB>)
-#
 #  -a: Only process customers with the specified agentnum
 #
 #  -v: enable debugging
 #
 #  -l: debugging level
 
+=item bill_where
+
+Internal function.  Returns a WHERE clause to select the set of customers who 
+have actionable packages (no setup date, or bill date in the past, or expire 
+or adjourn dates in the past) or events (does a complete where_conditions_sql 
+scan).
+
+=cut
+
 sub bill_where {
   my( %opt ) = @_;
 
@@ -159,8 +193,6 @@ sub bill_where {
 
   push @search, "( cust_main.archived != 'Y' OR archived IS NULL )"; #disable?
 
-  push @search, "cust_main.payby    = '". $opt{'p'}. "'"
-    if $opt{'p'};
   push @search, "cust_main.agentnum IN ( ". $opt{'a'}. " ) "
     if $opt{'a'};
 
@@ -176,33 +208,44 @@ sub bill_where {
   # generate where_pkg/where_event search clause
   ###
 
+  my $conf = new FS::Conf;
+  my $billtime = $conf->exists('next-bill-ignore-time') ? day_end($time) : $time;
+
+  # corresponds to perl checks in FS::cust_main::Billing sub bill
+  #  ("bill setup" and "bill recurring fee")
   # select * from cust_main where
   my $where_pkg = <<"END";
     EXISTS(
-      SELECT 1 FROM cust_pkg
+      SELECT 1 FROM cust_pkg LEFT JOIN part_pkg USING ( pkgpart )
         WHERE cust_main.custnum = cust_pkg.custnum
           AND ( cancel IS NULL OR cancel = 0 )
-          AND (    ( ( setup IS NULL OR setup =  0 )
+          AND (    ( ( cust_pkg.setup IS NULL OR cust_pkg.setup =  0 )
                      AND ( start_date IS NULL OR start_date = 0
-                           OR ( start_date IS NOT NULL AND start_date <= $^T )
+                           OR ( start_date IS NOT NULL AND start_date <= $billtime )
                          )
                    )
-                OR bill  IS NULL OR bill  <= $time 
+                OR ( freq != '0' AND ( bill IS NULL OR bill  <= $billtime ) )
                 OR ( expire  IS NOT NULL AND expire  <= $^T )
                 OR ( adjourn IS NOT NULL AND adjourn <= $^T )
+                OR ( resume  IS NOT NULL AND resume  <= $^T )
               )
     )
 END
 
+  #some false laziness w/cust_main::Billing due_cust_event
   my $where_event = join(' OR ', map {
     my $eventtable = $_;
 
-    my $join  = FS::part_event_condition->join_conditions_sql(  $eventtable );
+    # joins and where clauses to test event conditions
+    my $join  = FS::part_event_condition->join_conditions_sql(  $eventtable,
+                                                                'time'=>$time );
     my $where = FS::part_event_condition->where_conditions_sql( $eventtable,
                                                                 'time'=>$time,
                                                               );
     $where = $where ? "AND $where" : '';
 
+    # test to return all applicable part_events (defined on this eventtable,
+    # not disabled, check_freq correct, and all event conditions true)
     my $are_part_event = 
       "EXISTS ( SELECT 1 FROM part_event $join
                   WHERE check_freq = '$check_freq'
@@ -215,8 +258,11 @@ END
     if ( $eventtable eq 'cust_main' ) { 
       $are_part_event;
     } else {
-      "EXISTS ( SELECT 1 FROM $eventtable
-                  WHERE cust_main.custnum = $eventtable.custnum
+      my $cust_join = FS::part_event->eventtables_cust_join->{$eventtable}
+                      || '';
+      my $custnum = FS::part_event->eventtables_custnum->{$eventtable};
+      "EXISTS ( SELECT 1 FROM $eventtable $cust_join
+                  WHERE cust_main.custnum = $custnum
                     AND $are_part_event
               )
       ";