fix race condition where ->apply_payments_and_credits could double-apply in rare...
authorivan <ivan>
Fri, 13 Jul 2007 23:52:23 +0000 (23:52 +0000)
committerivan <ivan>
Fri, 13 Jul 2007 23:52:23 +0000 (23:52 +0000)
FS/FS/ClientAPI/Signup.pm
FS/FS/Cron/bill.pm
FS/FS/cust_bill.pm
FS/FS/cust_main.pm
FS/bin/freeside-prepaidd
httemplate/edit/process/cust_main.cgi
httemplate/misc/bill.cgi
httemplate/misc/process/recharge_svc.html

index e27f848..00b4d44 100644 (file)
@@ -439,7 +439,10 @@ sub new_customer {
     #warn "[fs_signup_server] error billing new customer: $bill_error"
     #  if $bill_error;
 
-    $cust_main->apply_payments_and_credits;
+    $bill_error = $cust_main->apply_payments_and_credits;
+    #warn "[fs_signup_server] error applying payments and credits for".
+    #     " new customer: $bill_error"
+    #  if $bill_error;
 
     $bill_error = $cust_main->collect('realtime' => 1);
     #warn "[fs_signup_server] error collecting from new customer: $bill_error"
index b6925d0..3ba1b53 100644 (file)
@@ -80,14 +80,16 @@ END
   
   my($cust_main,%saw);
   foreach $cust_main ( @cust_main ) {
+
+    my $custnum = $cust_main->custnum;
   
     # $^T not $time because -d is for pre-printing invoices
     foreach my $cust_pkg (
       grep { $_->expire && $_->expire <= $^T } $cust_main->ncancelled_pkgs
     ) {
       my $error = $cust_pkg->cancel;
-      warn "Error cancelling expired pkg ". $cust_pkg->pkgnum. " for custnum ".
-           $cust_main->custnum. ": $error"
+      warn "Error cancelling expired pkg ". $cust_pkg->pkgnum.
+           " for custnum $custnum: $error"
         if $error;
     }
     # $^T not $time because -d is for pre-printing invoices
@@ -102,8 +104,7 @@ END
       my $action = $cust_pkg->part_pkg->option('recur_action') || 'suspend';
       my $error = $cust_pkg->$action();
       warn "Error suspending package ". $cust_pkg->pkgnum.
-           " for custnum ". $cust_main->custnum.
-           ": $error"
+           " for custnum $custnum: $error"
         if $error;
     }
   
@@ -111,14 +112,16 @@ END
                                   'invoice_time' => $invoice_time,
                                   'resetup'      => $opt{'s'},
                                 );
-    warn "Error billing, custnum ". $cust_main->custnum. ": $error" if $error;
+    warn "Error billing, custnum $custnum: $error" if $error;
   
-    $cust_main->apply_payments_and_credits;
+    $error = $cust_main->apply_payments_and_credits;
+    warn "Error applying payments and credits, custnum $custnum: $error"
+      if $error;
   
     $error = $cust_main->collect( 'invoice_time' => $time,
                                   'freq'         => $opt{'freq'},
                                 );
-    warn "Error collecting, custnum". $cust_main->custnum. ": $error" if $error;
+    warn "Error collecting, custnum $custnum: $error" if $error;
   
   }
 
index ede9a0e..82023f6 100644 (file)
@@ -418,6 +418,19 @@ sub owed {
 sub apply_payments_and_credits {
   my $self = shift;
 
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $self->select_for_update; #mutex
+
   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
 
@@ -483,10 +496,17 @@ sub apply_payments_and_credits {
     $app->invnum( $self->invnum );
 
     my $error = $app->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error inserting ". $app->table. " record: $error";
+    }
     die $error if $error;
 
   }
 
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+
 }
 
 =item generate_email PARAMHASH
index b2b0a9b..7238e97 100644 (file)
@@ -3378,15 +3378,37 @@ Applies unapplied payments and credits.
 In most cases, this new method should be used in place of sequential
 apply_payments and apply_credits methods.
 
+If there is an error, returns the error, otherwise returns false.
+
 =cut
 
 sub apply_payments_and_credits {
   my $self = shift;
 
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $self->select_for_update; #mutex
+
   foreach my $cust_bill ( $self->open_cust_bill ) {
-    $cust_bill->apply_payments_and_credits;
+    my $error = $cust_bill->apply_payments_and_credits;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error applying: $error";
+    }
   }
 
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+
 }
 
 =item apply_credits OPTION => VALUE ...
@@ -3397,13 +3419,31 @@ chronological order if the I<order> option is set to B<newest>) and returns the
 value of any remaining unapplied credits available for refund (see
 L<FS::cust_refund>).
 
+Dies if there is an error.
+
 =cut
 
 sub apply_credits {
   my $self = shift;
   my %opt = @_;
 
-  return 0 unless $self->total_credited;
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $self->select_for_update; #mutex
+
+  unless ( $self->total_credited ) {
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    return 0;
+  }
 
   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
@@ -3432,13 +3472,20 @@ sub apply_credits {
       'amount'  => $amount,
     } );
     my $error = $cust_credit_bill->insert;
-    die $error if $error;
+    if ( $error ) {
+      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+      die $error;
+    }
     
     redo if ($cust_bill->owed > 0);
 
   }
 
-  return $self->total_credited;
+  my $total_credited = $self->total_credited;
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  return $total_credited;
 }
 
 =item apply_payments
@@ -3448,11 +3495,26 @@ to outstanding invoice balances in chronological order.
 
  #and returns the value of any remaining unapplied payments.
 
+Dies if there is an error.
+
 =cut
 
 sub apply_payments {
   my $self = shift;
 
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $self->select_for_update; #mutex
+
   #return 0 unless
 
   my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
@@ -3482,13 +3544,20 @@ sub apply_payments {
       'amount' => $amount,
     } );
     my $error = $cust_bill_pay->insert;
-    die $error if $error;
+    if ( $error ) {
+      $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
+      die $error;
+    }
 
     redo if ( $cust_bill->owed > 0);
 
   }
 
-  return $self->total_unapplied_payments;
+  my $total_unapplied_payments = $self->total_unapplied_payments;
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  return $total_unapplied_payments;
 }
 
 =item total_credited
@@ -4830,8 +4899,12 @@ sub batch_import {
         return "can't bill customer for $line: $error";
       }
   
-      $cust_main->apply_payments_and_credits;
-  
+      $error = $cust_main->apply_payments_and_credits;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "can't bill customer for $line: $error";
+      }
+
       $error = $cust_main->collect();
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
index 73f7523..a68db39 100644 (file)
@@ -52,9 +52,11 @@ while (1) {
         warn "Error billing customer #". $cust_main->custnum;
        next;
       }
-      #$b_error = $cust_main->apply_payments_and_credits;
-      $b_error = $cust_main->apply_payments;
-      $b_error = $cust_main->apply_credits;
+      $b_error = $cust_main->apply_payments_and_credits;
+      if ( $b_error ) {
+        warn "Error applying payments&credits, customer #". $cust_main->custnum;
+       next;
+      }
 
       $work_cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $work_cust_pkg->pkgnum } );
 
index b270fc6..b27e722 100755 (executable)
 %  my $conf = new FS::Conf;
 %  if ( $conf->exists('backend-realtime') && ! $error ) {
 %
-%    my $berror = $new->bill;
-%    $new->apply_payments_and_credits;
-%    $berror ||= $new->collect( 'realtime' => 1 );
+%    my $berror =    $new->bill
+%                 || $new->apply_payments_and_credits
+%                 || $new->collect( 'realtime' => 1 );
 %    warn "Warning, error billing during backend-realtime: $berror" if $berror;
 %
 %  }
index 8d4b4ac..1bf1eb1 100755 (executable)
 %#&eidiot($error) if $error;
 %
 %unless ( $error ) {
-%  $cust_main->apply_payments_and_credits;
-%
-%  $error = $cust_main->collect(
-%  #                             'invoice-time'=>$time,
-%                               #'batch_card'=> 'yes',
-%                               #'batch_card'=> 'no',
-%                               #'report_badcard'=> 'yes',
-%                               #'retry_card' => 'yes',
-%                               'retry' => 'yes',
-%                               'realtime' => $conf->exists('realtime-backend'),
-%                              );
+%  $error = $cust_main->apply_payments_and_credits
+%           || $cust_main->collect(
+%                                  #'invoice-time'=>$time,
+%                                  #'batch_card'=> 'yes',
+%                                  #'batch_card'=> 'no',
+%                                  #'report_badcard'=> 'yes',
+%                                  #'retry_card' => 'yes',
+%
+%                                  'retry' => 'yes',
+%                                   
+%                                  #this is used only by cust_main::batch_card
+%                                  #need to pick & create an actual config
+%                                  #value if we're going to turn this on
+%                                  #("realtime-backend" doesn't exist,
+%                                  # "backend-realtime" is for something
+%                                  #  entirely different)
+%                                  #'realtime' => $conf->exists('realtime-backend'),
+%                                 );
 %}
 %#&eidiot($error) if $error;
 %
@@ -38,4 +45,3 @@
 %  print $cgi->redirect(popurl(2). "view/cust_main.cgi?$custnum");
 %}
 %
-
index dc9d5ea..bc916e5 100755 (executable)
@@ -57,7 +57,7 @@
 %
 %    my $old_balance = $cust_main->balance;
 %    $error ||= $cust_main->bill;
-%    $cust_main->apply_payments_and_credits unless $error;
+%    $error ||= $cust_main->apply_payments_and_credits;
 %    my $bill_error = $cust_main->collect('realtime' => 1) unless $error;
 %    $error ||= "Failed to collect - $bill_error"
 %      if $cust_main->balance > $old_balance && $cust_main->balance > 0